@akash-chowdhury-24/deployhub 1.0.16 → 2.0.2
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/README.md +264 -59
- package/package.json +4 -3
- package/src/commands/doctor.js +168 -170
- package/src/commands/init.js +80 -371
- package/src/commands/rollback.js +1 -1
- package/src/core/config.js +17 -13
- package/src/core/stages.js +0 -11
- package/src/deployment/deployment-env.js +625 -0
- package/src/deployment/index.js +0 -72
- package/src/deployment/init-helpers.js +389 -0
- package/src/deployment/init-prompts.js +432 -0
- package/src/deployment/providers/azure-vm.js +85 -2
- package/src/deployment/providers/docker.js +101 -8
- package/src/deployment/providers/ec2.js +88 -2
- package/src/deployment/providers/gcp-vm.js +89 -2
- package/src/deployment/providers/kubernetes.js +104 -7
- package/src/deployment/providers/ssh.js +21 -4
- package/src/utils/github-actions.js +28 -131
- package/src/utils/rollback/engine.js +69 -0
- package/src/deployment/providers/platforms/_shared.js +0 -167
- package/src/deployment/providers/platforms/aws-amplify.js +0 -367
- package/src/deployment/providers/platforms/azure-static-web-apps.js +0 -68
- package/src/deployment/providers/platforms/cloudflare-pages.js +0 -103
- package/src/deployment/providers/platforms/firebase-app-hosting.js +0 -95
- package/src/deployment/providers/platforms/firebase-hosting.js +0 -99
- package/src/deployment/providers/platforms/index.js +0 -44
- package/src/deployment/providers/platforms/netlify.js +0 -102
- package/src/deployment/providers/platforms/vercel.js +0 -92
- package/src/rollback/engine.js +0 -102
- package/src/utils/firebase-config-generator.js +0 -35
- package/src/utils/init-platform.js +0 -236
- package/src/utils/platform-env.js +0 -132
|
@@ -1,167 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import archiver from 'archiver';
|
|
4
|
-
import { createLogger } from '../../../logger/index.js';
|
|
5
|
-
import {
|
|
6
|
-
runCli,
|
|
7
|
-
saveDeploymentRecord,
|
|
8
|
-
readPreviousPlatformDeployment,
|
|
9
|
-
checkUrlHealth,
|
|
10
|
-
} from './_shared.js';
|
|
11
|
-
|
|
12
|
-
const BLOCKING_AMPLIFY_JOB_STATUSES = new Set([
|
|
13
|
-
'CREATED',
|
|
14
|
-
'PENDING',
|
|
15
|
-
'PROVISIONING',
|
|
16
|
-
'RUNNING',
|
|
17
|
-
'CANCELLING',
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {number} ms
|
|
22
|
-
*/
|
|
23
|
-
function sleep(ms) {
|
|
24
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} sourceDir
|
|
29
|
-
* @param {string} zipPath
|
|
30
|
-
*/
|
|
31
|
-
async function createRootZip(sourceDir, zipPath) {
|
|
32
|
-
await fs.ensureDir(path.dirname(zipPath));
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
const output = fs.createWriteStream(zipPath);
|
|
35
|
-
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
36
|
-
|
|
37
|
-
output.on('close', resolve);
|
|
38
|
-
archive.on('error', reject);
|
|
39
|
-
|
|
40
|
-
archive.pipe(output);
|
|
41
|
-
archive.directory(sourceDir, false);
|
|
42
|
-
archive.finalize();
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @param {string} zipPath
|
|
48
|
-
* @param {string} uploadUrl
|
|
49
|
-
*/
|
|
50
|
-
async function uploadZipToAmplify(zipPath, uploadUrl) {
|
|
51
|
-
const body = await fs.readFile(zipPath);
|
|
52
|
-
const response = await fetch(uploadUrl, {
|
|
53
|
-
method: 'PUT',
|
|
54
|
-
body,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (!response.ok) {
|
|
58
|
-
const detail = await response.text().catch(() => '');
|
|
59
|
-
throw new Error(
|
|
60
|
-
`Amplify zip upload failed: HTTP ${response.status}${detail ? ` — ${detail}` : ''}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {import('../../../core/config.js').DeployHubConfig} config
|
|
67
|
-
* @param {string} envName
|
|
68
|
-
* @param {Record<string, string>} [env]
|
|
69
|
-
*/
|
|
70
|
-
export function createAwsAmplifyProvider(config, envName, env = process.env) {
|
|
71
|
-
const environment = config.environments[envName];
|
|
72
|
-
const log = createLogger('aws-amplify');
|
|
73
|
-
const appId = environment?.appId || env.AMPLIFY_APP_ID;
|
|
74
|
-
const region = environment?.region || env.AWS_REGION || 'us-east-1';
|
|
75
|
-
const branch = environment?.branch || env.AMPLIFY_BRANCH || 'main';
|
|
76
|
-
const githubConnected = environment?.githubConnected ?? false;
|
|
77
|
-
|
|
78
|
-
function getAwsEnv() {
|
|
79
|
-
return {
|
|
80
|
-
AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
|
|
81
|
-
AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
|
|
82
|
-
AWS_DEFAULT_REGION: region,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {string} stage
|
|
88
|
-
*/
|
|
89
|
-
function resolveAmplifyStage(stage) {
|
|
90
|
-
const normalized = stage.toLowerCase();
|
|
91
|
-
if (normalized === 'production') return 'PRODUCTION';
|
|
92
|
-
if (normalized === 'staging') return 'BETA';
|
|
93
|
-
return 'DEVELOPMENT';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function branchExists(awsEnv) {
|
|
97
|
-
const result = await runCli(
|
|
98
|
-
`aws amplify get-branch --app-id ${appId} --branch-name ${branch}`,
|
|
99
|
-
process.cwd(),
|
|
100
|
-
awsEnv
|
|
101
|
-
);
|
|
102
|
-
return result.exitCode === 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function ensureBranchExists(awsEnv) {
|
|
106
|
-
if (await branchExists(awsEnv)) return;
|
|
107
|
-
|
|
108
|
-
if (githubConnected) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Amplify branch "${branch}" not found. Connect your GitHub repo in the Amplify console or choose a branch that already exists.`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const stage = resolveAmplifyStage(envName);
|
|
115
|
-
log.info(`Creating Amplify branch "${branch}" for manual zip deploys...`);
|
|
116
|
-
const createResult = await runCli(
|
|
117
|
-
`aws amplify create-branch --app-id ${appId} --branch-name ${branch} --stage ${stage} --no-enable-auto-build --description "Created by DeployHub for manual deployments"`,
|
|
118
|
-
process.cwd(),
|
|
119
|
-
awsEnv
|
|
120
|
-
);
|
|
121
|
-
if (createResult.exitCode !== 0) {
|
|
122
|
-
throw new Error(`Amplify create-branch failed: ${createResult.stderr || createResult.stdout}`);
|
|
123
|
-
}
|
|
124
|
-
log.success(`Amplify branch "${branch}" ready`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @param {Record<string, string>} awsEnv
|
|
129
|
-
* @returns {Promise<{ jobId?: string, status?: string }[]>}
|
|
130
|
-
*/
|
|
131
|
-
async function listBranchJobs(awsEnv) {
|
|
132
|
-
const result = await runCli(
|
|
133
|
-
`aws amplify list-jobs --app-id ${appId} --branch-name ${branch} --max-items 10 --output json`,
|
|
134
|
-
process.cwd(),
|
|
135
|
-
awsEnv
|
|
136
|
-
);
|
|
137
|
-
if (result.exitCode !== 0) return [];
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const data = JSON.parse(result.stdout);
|
|
141
|
-
return data.jobSummaries || [];
|
|
142
|
-
} catch {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Cancel stuck jobs left by failed uploads so create-deployment can proceed.
|
|
149
|
-
* @param {Record<string, string>} awsEnv
|
|
150
|
-
*/
|
|
151
|
-
async function clearBlockingAmplifyJobs(awsEnv) {
|
|
152
|
-
const jobs = await listBranchJobs(awsEnv);
|
|
153
|
-
const blocking = jobs.filter((job) =>
|
|
154
|
-
BLOCKING_AMPLIFY_JOB_STATUSES.has(job.status || '')
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
if (blocking.length === 0) return;
|
|
158
|
-
|
|
159
|
-
for (const job of blocking) {
|
|
160
|
-
log.info(`Stopping in-progress Amplify job ${job.jobId} (${job.status})...`);
|
|
161
|
-
const stopResult = await runCli(
|
|
162
|
-
`aws amplify stop-job --app-id ${appId} --branch-name ${branch} --job-id ${job.jobId}`,
|
|
163
|
-
process.cwd(),
|
|
164
|
-
awsEnv
|
|
165
|
-
);
|
|
166
|
-
if (stopResult.exitCode !== 0) {
|
|
167
|
-
log.warn(
|
|
168
|
-
`Could not stop Amplify job ${job.jobId}: ${stopResult.stderr || stopResult.stdout}`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
174
|
-
await sleep(5000);
|
|
175
|
-
const current = await listBranchJobs(awsEnv);
|
|
176
|
-
const stillBlocking = current.filter((job) =>
|
|
177
|
-
BLOCKING_AMPLIFY_JOB_STATUSES.has(job.status || '')
|
|
178
|
-
);
|
|
179
|
-
if (stillBlocking.length === 0) {
|
|
180
|
-
log.info('Amplify branch is ready for a new deployment');
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
throw new Error(
|
|
186
|
-
'Timed out waiting for in-progress Amplify jobs to finish. Stop them in the AWS console and retry.'
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* @param {Record<string, string>} awsEnv
|
|
192
|
-
* @returns {Promise<Record<string, unknown>>}
|
|
193
|
-
*/
|
|
194
|
-
async function createManualDeployment(awsEnv) {
|
|
195
|
-
await clearBlockingAmplifyJobs(awsEnv);
|
|
196
|
-
|
|
197
|
-
const createResult = await runCli(
|
|
198
|
-
`aws amplify create-deployment --app-id ${appId} --branch-name ${branch} --output json`,
|
|
199
|
-
process.cwd(),
|
|
200
|
-
awsEnv
|
|
201
|
-
);
|
|
202
|
-
if (createResult.exitCode !== 0) {
|
|
203
|
-
const errorText = `${createResult.stderr || ''}${createResult.stdout || ''}`;
|
|
204
|
-
if (/was not finished/i.test(errorText)) {
|
|
205
|
-
log.warn('Amplify has a stuck deployment — clearing it and retrying...');
|
|
206
|
-
await clearBlockingAmplifyJobs(awsEnv);
|
|
207
|
-
const retryResult = await runCli(
|
|
208
|
-
`aws amplify create-deployment --app-id ${appId} --branch-name ${branch} --output json`,
|
|
209
|
-
process.cwd(),
|
|
210
|
-
awsEnv
|
|
211
|
-
);
|
|
212
|
-
if (retryResult.exitCode !== 0) {
|
|
213
|
-
throw new Error(`Amplify create-deployment failed: ${retryResult.stderr}`);
|
|
214
|
-
}
|
|
215
|
-
return JSON.parse(retryResult.stdout);
|
|
216
|
-
}
|
|
217
|
-
throw new Error(`Amplify create-deployment failed: ${createResult.stderr}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return JSON.parse(createResult.stdout);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function deploy(artifactDir) {
|
|
224
|
-
if (!appId) throw new Error('AMPLIFY_APP_ID is required');
|
|
225
|
-
|
|
226
|
-
const awsEnv = getAwsEnv();
|
|
227
|
-
|
|
228
|
-
if (githubConnected) {
|
|
229
|
-
await ensureBranchExists(awsEnv);
|
|
230
|
-
log.info(`Triggering Amplify release job on branch ${branch} (GitHub connected)...`);
|
|
231
|
-
const result = await runCli(
|
|
232
|
-
`aws amplify start-job --app-id ${appId} --branch-name ${branch} --job-type RELEASE`,
|
|
233
|
-
process.cwd(),
|
|
234
|
-
awsEnv
|
|
235
|
-
);
|
|
236
|
-
if (result.exitCode !== 0) {
|
|
237
|
-
throw new Error(`Amplify start-job failed: ${result.stderr || result.stdout}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const jobMatch = (result.stdout || '').match(/"jobId":\s*"([^"]+)"/);
|
|
241
|
-
const jobId = jobMatch?.[1] || '';
|
|
242
|
-
|
|
243
|
-
await saveDeploymentRecord(
|
|
244
|
-
artifactDir,
|
|
245
|
-
{
|
|
246
|
-
platform: 'aws-amplify',
|
|
247
|
-
deploymentId: jobId,
|
|
248
|
-
appId,
|
|
249
|
-
branch,
|
|
250
|
-
method: 'github',
|
|
251
|
-
},
|
|
252
|
-
envName
|
|
253
|
-
);
|
|
254
|
-
log.success(`Amplify job started: ${jobId || 'RELEASE'}`);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
log.info(`Uploading build output to Amplify branch ${branch}...`);
|
|
259
|
-
await ensureBranchExists(awsEnv);
|
|
260
|
-
|
|
261
|
-
const buildOutput = config.frontend?.buildOutput || config.buildOutput || 'dist';
|
|
262
|
-
const buildDir = path.join(process.cwd(), buildOutput);
|
|
263
|
-
if (!(await fs.pathExists(buildDir))) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
`Build output not found at ${buildOutput}. Amplify needs the built static files, not artifact.zip with a nested folder.`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const amplifyZipPath = path.join(artifactDir, 'amplify-deploy.zip');
|
|
270
|
-
await createRootZip(buildDir, amplifyZipPath);
|
|
271
|
-
|
|
272
|
-
const deployment = await createManualDeployment(awsEnv);
|
|
273
|
-
const jobId = deployment.jobId;
|
|
274
|
-
const zipUploadUrl = deployment.zipUploadUrl;
|
|
275
|
-
|
|
276
|
-
log.info('Uploading zip to Amplify...');
|
|
277
|
-
await uploadZipToAmplify(amplifyZipPath, zipUploadUrl);
|
|
278
|
-
|
|
279
|
-
const startResult = await runCli(
|
|
280
|
-
`aws amplify start-deployment --app-id ${appId} --branch-name ${branch} --job-id ${jobId}`,
|
|
281
|
-
process.cwd(),
|
|
282
|
-
awsEnv
|
|
283
|
-
);
|
|
284
|
-
if (startResult.exitCode !== 0) {
|
|
285
|
-
throw new Error(`Amplify start-deployment failed: ${startResult.stderr}`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
await fs.remove(amplifyZipPath);
|
|
289
|
-
|
|
290
|
-
await saveDeploymentRecord(
|
|
291
|
-
artifactDir,
|
|
292
|
-
{
|
|
293
|
-
platform: 'aws-amplify',
|
|
294
|
-
deploymentId: jobId,
|
|
295
|
-
appId,
|
|
296
|
-
branch,
|
|
297
|
-
method: 'zip',
|
|
298
|
-
},
|
|
299
|
-
envName
|
|
300
|
-
);
|
|
301
|
-
log.success(`Amplify deployment started: job ${jobId}`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function rollback(artifactDir) {
|
|
305
|
-
if (!appId) throw new Error('AMPLIFY_APP_ID is required');
|
|
306
|
-
|
|
307
|
-
const previous = await readPreviousPlatformDeployment(artifactDir);
|
|
308
|
-
const jobId = previous?.deploymentId;
|
|
309
|
-
if (!jobId) {
|
|
310
|
-
throw new Error('No previous Amplify job ID found for rollback');
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const awsEnv = getAwsEnv();
|
|
314
|
-
const rollbackBranch = previous?.branch || branch;
|
|
315
|
-
|
|
316
|
-
log.info(`Re-triggering Amplify job ${jobId} on branch ${rollbackBranch}...`);
|
|
317
|
-
const result = await runCli(
|
|
318
|
-
`aws amplify start-job --app-id ${appId} --branch-name ${rollbackBranch} --job-id ${jobId} --job-type RETRY`,
|
|
319
|
-
process.cwd(),
|
|
320
|
-
awsEnv
|
|
321
|
-
);
|
|
322
|
-
if (result.exitCode !== 0) {
|
|
323
|
-
await runCli(
|
|
324
|
-
`aws amplify start-job --app-id ${appId} --branch-name ${rollbackBranch} --job-type RELEASE`,
|
|
325
|
-
process.cwd(),
|
|
326
|
-
awsEnv
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
log.success('Amplify rollback triggered');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function healthCheck() {
|
|
333
|
-
const url = config.healthCheck?.url;
|
|
334
|
-
if (!url) return true;
|
|
335
|
-
return checkUrlHealth(url);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async function testConnection() {
|
|
339
|
-
if (!appId) throw new Error('AMPLIFY_APP_ID is required');
|
|
340
|
-
|
|
341
|
-
const awsEnv = getAwsEnv();
|
|
342
|
-
const appResult = await runCli(
|
|
343
|
-
`aws amplify get-app --app-id ${appId}`,
|
|
344
|
-
process.cwd(),
|
|
345
|
-
awsEnv
|
|
346
|
-
);
|
|
347
|
-
if (appResult.exitCode !== 0) {
|
|
348
|
-
throw new Error('Could not find Amplify app — check AMPLIFY_APP_ID and AWS credentials');
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (await branchExists(awsEnv)) return;
|
|
352
|
-
|
|
353
|
-
if (githubConnected) {
|
|
354
|
-
throw new Error(
|
|
355
|
-
`Amplify branch "${branch}" not found. Connect your GitHub repo in the Amplify console or choose a branch that already exists.`
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
log.info(
|
|
360
|
-
`Amplify branch "${branch}" will be created automatically on the first deploy`
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return { deploy, rollback, healthCheck, testConnection };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export default { createAwsAmplifyProvider };
|
|
@@ -1,68 +0,0 @@
|
|
|
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 };
|
|
@@ -1,103 +0,0 @@
|
|
|
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 };
|