@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.
Files changed (32) hide show
  1. package/README.md +264 -59
  2. package/package.json +4 -3
  3. package/src/commands/doctor.js +168 -170
  4. package/src/commands/init.js +80 -371
  5. package/src/commands/rollback.js +1 -1
  6. package/src/core/config.js +17 -13
  7. package/src/core/stages.js +0 -11
  8. package/src/deployment/deployment-env.js +625 -0
  9. package/src/deployment/index.js +0 -72
  10. package/src/deployment/init-helpers.js +389 -0
  11. package/src/deployment/init-prompts.js +432 -0
  12. package/src/deployment/providers/azure-vm.js +85 -2
  13. package/src/deployment/providers/docker.js +101 -8
  14. package/src/deployment/providers/ec2.js +88 -2
  15. package/src/deployment/providers/gcp-vm.js +89 -2
  16. package/src/deployment/providers/kubernetes.js +104 -7
  17. package/src/deployment/providers/ssh.js +21 -4
  18. package/src/utils/github-actions.js +28 -131
  19. package/src/utils/rollback/engine.js +69 -0
  20. package/src/deployment/providers/platforms/_shared.js +0 -167
  21. package/src/deployment/providers/platforms/aws-amplify.js +0 -367
  22. package/src/deployment/providers/platforms/azure-static-web-apps.js +0 -68
  23. package/src/deployment/providers/platforms/cloudflare-pages.js +0 -103
  24. package/src/deployment/providers/platforms/firebase-app-hosting.js +0 -95
  25. package/src/deployment/providers/platforms/firebase-hosting.js +0 -99
  26. package/src/deployment/providers/platforms/index.js +0 -44
  27. package/src/deployment/providers/platforms/netlify.js +0 -102
  28. package/src/deployment/providers/platforms/vercel.js +0 -92
  29. package/src/rollback/engine.js +0 -102
  30. package/src/utils/firebase-config-generator.js +0 -35
  31. package/src/utils/init-platform.js +0 -236
  32. 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 };