@akash-chowdhury-24/deployhub 2.0.14 → 2.0.16
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 +98 -25
- package/package.json +1 -1
- package/src/artifact/engine.js +11 -4
- package/src/cli/index.js +4 -0
- package/src/commands/deploy.js +61 -16
- package/src/commands/doctor.js +212 -26
- package/src/commands/env.js +326 -0
- package/src/commands/init.js +97 -64
- package/src/commands/rollback.js +84 -24
- package/src/commands/sync-k8s-ports.js +71 -0
- package/src/commands/verify.js +97 -33
- package/src/core/config.js +371 -33
- package/src/core/environments.js +300 -0
- package/src/core/stages.js +55 -19
- package/src/deployment/deployment-env.js +210 -0
- package/src/deployment/index.js +58 -7
- package/src/deployment/init-prompts.js +151 -46
- package/src/deployment/providers/azure-vm.js +8 -10
- package/src/deployment/providers/docker.js +10 -2
- package/src/deployment/providers/ec2.js +7 -9
- package/src/deployment/providers/gcp-vm.js +8 -10
- package/src/deployment/providers/kubernetes.js +15 -6
- package/src/deployment/providers/ssh.js +18 -13
- package/src/storage/index.js +230 -3
- package/src/utils/build-id.js +23 -0
- package/src/utils/dockerfile-expose.js +117 -0
- package/src/utils/github-actions.js +344 -31
- package/src/utils/health-check.js +158 -0
- package/src/utils/kubernetes-manifests.js +105 -10
- package/src/utils/nginx.js +66 -15
- package/src/utils/rollback/engine.js +147 -40
- package/src/utils/scaffold.js +8 -3
package/src/storage/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
import { createLogger } from '../logger/index.js';
|
|
5
6
|
import { createAwsProvider } from './providers/aws.js';
|
|
6
7
|
import { createLocalProvider } from './providers/local.js';
|
|
@@ -11,6 +12,8 @@ import { createDropboxProvider } from './providers/dropbox.js';
|
|
|
11
12
|
import { createFtpProvider } from './providers/ftp.js';
|
|
12
13
|
import {
|
|
13
14
|
buildArtifactRemoteKey,
|
|
15
|
+
envHistoryRemoteKey,
|
|
16
|
+
envLatestArtifactRemoteKey,
|
|
14
17
|
historyRemoteKey,
|
|
15
18
|
latestArtifactRemoteKey,
|
|
16
19
|
legacyArtifactRemoteKey,
|
|
@@ -22,6 +25,16 @@ import {
|
|
|
22
25
|
} from '../utils/artifact-history.js';
|
|
23
26
|
import { summarizeStorageError } from './storage-errors.js';
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Unique temp path — never Date.now() alone (collides across concurrent
|
|
30
|
+
* processes / Jest workers in the same millisecond and corrupts history reads).
|
|
31
|
+
* @param {string} prefix
|
|
32
|
+
* @param {string} [ext]
|
|
33
|
+
*/
|
|
34
|
+
function uniqueTmpPath(prefix, ext = '.json') {
|
|
35
|
+
return path.join(os.tmpdir(), `${prefix}-${randomUUID()}${ext}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/** @type {Record<string, (env?: Record<string, string>) => ReturnType<typeof createAwsProvider>>} */
|
|
26
39
|
const PROVIDER_FACTORIES = {
|
|
27
40
|
aws: createAwsProvider,
|
|
@@ -77,7 +90,7 @@ export async function loadArtifactHistory(providers, project) {
|
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
const key = historyRemoteKey(project);
|
|
80
|
-
const tmp =
|
|
93
|
+
const tmp = uniqueTmpPath('deployhub-history');
|
|
81
94
|
|
|
82
95
|
try {
|
|
83
96
|
for (const name of providers) {
|
|
@@ -114,7 +127,24 @@ export async function loadArtifactHistory(providers, project) {
|
|
|
114
127
|
*/
|
|
115
128
|
async function writeArtifactHistory(provider, project, history) {
|
|
116
129
|
const key = historyRemoteKey(project);
|
|
117
|
-
const tmp =
|
|
130
|
+
const tmp = uniqueTmpPath('deployhub-history-write');
|
|
131
|
+
await fs.writeJson(tmp, history, { spaces: 2 });
|
|
132
|
+
try {
|
|
133
|
+
await provider.upload(tmp, key);
|
|
134
|
+
} finally {
|
|
135
|
+
await fs.remove(tmp).catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {ReturnType<typeof getStorageProvider>} provider
|
|
141
|
+
* @param {string} project
|
|
142
|
+
* @param {string} envName
|
|
143
|
+
* @param {import('../utils/artifact-history.js').ArtifactHistoryEntry[]} history
|
|
144
|
+
*/
|
|
145
|
+
async function writeEnvArtifactHistory(provider, project, envName, history) {
|
|
146
|
+
const key = envHistoryRemoteKey(project, envName);
|
|
147
|
+
const tmp = uniqueTmpPath('deployhub-env-hist-write');
|
|
118
148
|
await fs.writeJson(tmp, history, { spaces: 2 });
|
|
119
149
|
try {
|
|
120
150
|
await provider.upload(tmp, key);
|
|
@@ -123,6 +153,200 @@ async function writeArtifactHistory(provider, project, history) {
|
|
|
123
153
|
}
|
|
124
154
|
}
|
|
125
155
|
|
|
156
|
+
function legacyHistoryCopiedMarkerKey(project) {
|
|
157
|
+
return `${project}/.legacy-env-history-copied`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Record that legacy history migration was handled (copy attempted or not needed).
|
|
162
|
+
* Prevents later rollback from copying the build catalog into the default env.
|
|
163
|
+
*
|
|
164
|
+
* @param {string[]} providers
|
|
165
|
+
* @param {string} project
|
|
166
|
+
*/
|
|
167
|
+
async function markLegacyHistoryMigrationHandled(providers, project) {
|
|
168
|
+
if (!providers?.length || !project) return;
|
|
169
|
+
for (const name of providers) {
|
|
170
|
+
const provider = getStorageProvider(name);
|
|
171
|
+
if (await provider.verify(legacyHistoryCopiedMarkerKey(project))) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const markerTmp = uniqueTmpPath('deployhub-legacy-copied', '.txt');
|
|
175
|
+
await fs.writeFile(markerTmp, '1');
|
|
176
|
+
try {
|
|
177
|
+
await provider.upload(markerTmp, legacyHistoryCopiedMarkerKey(project));
|
|
178
|
+
} finally {
|
|
179
|
+
await fs.remove(markerTmp).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One-time copy of legacy `{project}/history.json` into the default environment's
|
|
186
|
+
* `envs/{default}/history.json`. Separate from `recordEnvDeployment` (which never
|
|
187
|
+
* seeds from the project build catalog).
|
|
188
|
+
*
|
|
189
|
+
* Guard scope (judgment call — see comment in source):
|
|
190
|
+
* Only skips when the default env's own history key already exists, or when migration
|
|
191
|
+
* was already handled (storage marker / config.legacyHistoryMigrated). We do NOT skip
|
|
192
|
+
* just because a sibling env (e.g. testing) has history — that would lock default out
|
|
193
|
+
* of recovering real pre-multi-env deploy history after shape migration.
|
|
194
|
+
*
|
|
195
|
+
* @param {string[]} providers
|
|
196
|
+
* @param {string} project
|
|
197
|
+
* @param {string} defaultEnvironment
|
|
198
|
+
* @param {{ legacyHistoryMigrated?: boolean }} [options]
|
|
199
|
+
* @returns {Promise<boolean>} true when a copy was written
|
|
200
|
+
*/
|
|
201
|
+
export async function ensureLegacyHistoryCopiedToDefaultEnv(
|
|
202
|
+
providers,
|
|
203
|
+
project,
|
|
204
|
+
defaultEnvironment,
|
|
205
|
+
options = {}
|
|
206
|
+
) {
|
|
207
|
+
if (!providers?.length || !project || !defaultEnvironment) return false;
|
|
208
|
+
if (options.legacyHistoryMigrated === true) return false;
|
|
209
|
+
|
|
210
|
+
for (const name of providers) {
|
|
211
|
+
const provider = getStorageProvider(name);
|
|
212
|
+
if (await provider.verify(legacyHistoryCopiedMarkerKey(project))) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const envKey = envHistoryRemoteKey(project, defaultEnvironment);
|
|
218
|
+
for (const name of providers) {
|
|
219
|
+
const provider = getStorageProvider(name);
|
|
220
|
+
if (await provider.verify(envKey)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const legacy = await loadArtifactHistory(providers, project);
|
|
226
|
+
if (legacy.entries.length === 0) {
|
|
227
|
+
await markLegacyHistoryMigrationHandled(providers, project);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const log = createLogger('storage');
|
|
232
|
+
log.info(
|
|
233
|
+
`Migrating legacy deploy history into env "${defaultEnvironment}" (${legacy.entries.length} entries)...`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
for (const name of providers) {
|
|
237
|
+
const provider = getStorageProvider(name);
|
|
238
|
+
await writeEnvArtifactHistory(provider, project, defaultEnvironment, legacy.entries);
|
|
239
|
+
}
|
|
240
|
+
await markLegacyHistoryMigrationHandled(providers, project);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Load per-environment deploy history.
|
|
246
|
+
* Legacy project-wide `{project}/history.json` is NOT merged here during rollback
|
|
247
|
+
* (use `ensureLegacyHistoryCopiedToDefaultEnv` first). Read-only legacy fallback for
|
|
248
|
+
* list/doctor when `allowLegacyFallback` is true.
|
|
249
|
+
*
|
|
250
|
+
* @param {string[]} providers
|
|
251
|
+
* @param {string} project
|
|
252
|
+
* @param {string} envName
|
|
253
|
+
* @param {{ defaultEnvironment?: string|null, allowLegacyFallback?: boolean }} [options]
|
|
254
|
+
*/
|
|
255
|
+
export async function loadEnvArtifactHistory(providers, project, envName, options = {}) {
|
|
256
|
+
if (!providers || providers.length === 0) {
|
|
257
|
+
return { entries: [], source: null, key: envHistoryRemoteKey(project, envName) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const envKey = envHistoryRemoteKey(project, envName);
|
|
261
|
+
const tmp = uniqueTmpPath('deployhub-env-history');
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
for (const name of providers) {
|
|
265
|
+
try {
|
|
266
|
+
const provider = getStorageProvider(name);
|
|
267
|
+
const exists = await provider.verify(envKey);
|
|
268
|
+
if (!exists) continue;
|
|
269
|
+
|
|
270
|
+
await provider.download(envKey, tmp);
|
|
271
|
+
const raw = await fs.readFile(tmp, 'utf8');
|
|
272
|
+
return {
|
|
273
|
+
entries: parseArtifactHistory(raw),
|
|
274
|
+
source: name,
|
|
275
|
+
key: envKey,
|
|
276
|
+
};
|
|
277
|
+
} catch (err) {
|
|
278
|
+
const reason = summarizeStorageError(err);
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Could not check remote history via ${name}: ${reason} — ` +
|
|
281
|
+
'verify your storage credentials and configuration are correct.'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} finally {
|
|
286
|
+
await fs.remove(tmp).catch(() => {});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const defaultName =
|
|
290
|
+
typeof options.defaultEnvironment === 'string' ? options.defaultEnvironment : null;
|
|
291
|
+
const allowLegacy =
|
|
292
|
+
options.allowLegacyFallback !== false &&
|
|
293
|
+
(envName === 'default' || (defaultName !== null && envName === defaultName));
|
|
294
|
+
|
|
295
|
+
if (allowLegacy) {
|
|
296
|
+
const legacy = await loadArtifactHistory(providers, project);
|
|
297
|
+
if (legacy.entries.length > 0) {
|
|
298
|
+
return {
|
|
299
|
+
entries: legacy.entries,
|
|
300
|
+
source: legacy.source,
|
|
301
|
+
key: historyRemoteKey(project),
|
|
302
|
+
legacyFallback: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { entries: [], source: null, key: envKey };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Record a successful deploy into per-env history + env latest pointer.
|
|
312
|
+
* Does not touch other environments' history.
|
|
313
|
+
*
|
|
314
|
+
* @param {string[]} providers
|
|
315
|
+
* @param {string} zipPath path to artifact.zip to copy into envs/{env}/latest/
|
|
316
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
317
|
+
* @param {string} envName
|
|
318
|
+
* @param {{ buildId: string, semver?: string, remoteKey?: string }} meta
|
|
319
|
+
*/
|
|
320
|
+
export async function recordEnvDeployment(providers, zipPath, config, envName, meta) {
|
|
321
|
+
const log = createLogger('storage');
|
|
322
|
+
const buildId = meta.buildId;
|
|
323
|
+
const remoteKey =
|
|
324
|
+
meta.remoteKey || buildArtifactRemoteKey(config.project, buildId);
|
|
325
|
+
const entry = {
|
|
326
|
+
buildId,
|
|
327
|
+
semver: String(meta.semver || config.version || '0.0.0').replace(/^v/i, ''),
|
|
328
|
+
uploadedAt: new Date().toISOString(),
|
|
329
|
+
remoteKey,
|
|
330
|
+
};
|
|
331
|
+
const envLatestKey = envLatestArtifactRemoteKey(config.project, envName);
|
|
332
|
+
|
|
333
|
+
for (const name of providers) {
|
|
334
|
+
const provider = getStorageProvider(name);
|
|
335
|
+
log.info(`Recording deploy of ${buildId} to env "${envName}" on ${name}...`);
|
|
336
|
+
|
|
337
|
+
if (zipPath && (await fs.pathExists(zipPath))) {
|
|
338
|
+
await provider.upload(zipPath, envLatestKey);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const loaded = await loadEnvArtifactHistory([name], config.project, envName, {
|
|
342
|
+
// Never seed deploy history from the project-wide build catalog on record.
|
|
343
|
+
allowLegacyFallback: false,
|
|
344
|
+
});
|
|
345
|
+
const next = prependHistoryEntry(loaded.entries, entry);
|
|
346
|
+
await writeEnvArtifactHistory(provider, config.project, envName, next);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
126
350
|
/**
|
|
127
351
|
* Upload artifact zip under a unique build key, update latest/ pointer and history.json.
|
|
128
352
|
* Does NOT write legacy `{project}/v{semver}/artifact.zip` (retired for new uploads;
|
|
@@ -158,7 +382,7 @@ export async function uploadToAll(providers, zipPath, config) {
|
|
|
158
382
|
try {
|
|
159
383
|
const histKey = historyRemoteKey(config.project);
|
|
160
384
|
if (await provider.verify(histKey)) {
|
|
161
|
-
const tmp =
|
|
385
|
+
const tmp = uniqueTmpPath(`deployhub-hist-${name}`);
|
|
162
386
|
try {
|
|
163
387
|
await provider.download(histKey, tmp);
|
|
164
388
|
history = parseArtifactHistory(await fs.readFile(tmp, 'utf8'));
|
|
@@ -260,6 +484,9 @@ export default {
|
|
|
260
484
|
downloadFromFirst,
|
|
261
485
|
downloadArtifactEntry,
|
|
262
486
|
loadArtifactHistory,
|
|
487
|
+
loadEnvArtifactHistory,
|
|
488
|
+
ensureLegacyHistoryCopiedToDefaultEnv,
|
|
489
|
+
recordEnvDeployment,
|
|
263
490
|
testProvider,
|
|
264
491
|
testAllProviders,
|
|
265
492
|
};
|
package/src/utils/build-id.js
CHANGED
|
@@ -122,12 +122,33 @@ export function latestArtifactRemoteKey(project) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
+
* Project-wide build catalog (newest-first). Legacy pre-multi-env history;
|
|
126
|
+
* also still written on upload as the available-builds index for `artifact list --remote`.
|
|
127
|
+
* Per-environment deploy history lives under envs/{env}/history.json.
|
|
125
128
|
* @param {string} project
|
|
126
129
|
*/
|
|
127
130
|
export function historyRemoteKey(project) {
|
|
128
131
|
return `${project}/history.json`;
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Per-environment deploy history (newest-first). Rollback resolves against this only.
|
|
136
|
+
* @param {string} project
|
|
137
|
+
* @param {string} envName
|
|
138
|
+
*/
|
|
139
|
+
export function envHistoryRemoteKey(project, envName) {
|
|
140
|
+
return `${project}/envs/${sanitizeBuildIdPart(envName)}/history.json`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Per-environment "currently deployed" pointer (NOT a backup — same semantics as project latest/).
|
|
145
|
+
* @param {string} project
|
|
146
|
+
* @param {string} envName
|
|
147
|
+
*/
|
|
148
|
+
export function envLatestArtifactRemoteKey(project, envName) {
|
|
149
|
+
return `${project}/envs/${sanitizeBuildIdPart(envName)}/latest/artifact.zip`;
|
|
150
|
+
}
|
|
151
|
+
|
|
131
152
|
/**
|
|
132
153
|
* Pre-buildId legacy key (read-only fallback; never written by new uploads).
|
|
133
154
|
* @param {string} project
|
|
@@ -145,5 +166,7 @@ export default {
|
|
|
145
166
|
buildArtifactRemoteKey,
|
|
146
167
|
latestArtifactRemoteKey,
|
|
147
168
|
historyRemoteKey,
|
|
169
|
+
envHistoryRemoteKey,
|
|
170
|
+
envLatestArtifactRemoteKey,
|
|
148
171
|
legacyArtifactRemoteKey,
|
|
149
172
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/** Static SPA frameworks that use nginx:alpine + EXPOSE 80 in generated Dockerfiles. */
|
|
5
|
+
const STATIC_FRONTEND_FRAMEWORKS = new Set([
|
|
6
|
+
'react',
|
|
7
|
+
'vue',
|
|
8
|
+
'angular',
|
|
9
|
+
'svelte',
|
|
10
|
+
'astro',
|
|
11
|
+
'vanilla',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse the listening port from Dockerfile EXPOSE instructions.
|
|
16
|
+
* Uses the last EXPOSE line (final stage in multi-stage builds).
|
|
17
|
+
* Supports `EXPOSE 80`, `EXPOSE 80/tcp`, and multi-port lines (first numeric wins on that line).
|
|
18
|
+
*
|
|
19
|
+
* @param {string} content
|
|
20
|
+
* @returns {number|null}
|
|
21
|
+
*/
|
|
22
|
+
export function parseDockerfileExposePort(content) {
|
|
23
|
+
if (!content || typeof content !== 'string') return null;
|
|
24
|
+
|
|
25
|
+
/** @type {number|null} */
|
|
26
|
+
let lastPort = null;
|
|
27
|
+
|
|
28
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
29
|
+
const line = rawLine.trim();
|
|
30
|
+
if (!line || line.startsWith('#')) continue;
|
|
31
|
+
|
|
32
|
+
const match = line.match(/^EXPOSE\s+(.+)$/i);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
|
|
35
|
+
const tokens = match[1].trim().split(/\s+/);
|
|
36
|
+
for (const token of tokens) {
|
|
37
|
+
const portToken = token.split('/')[0];
|
|
38
|
+
if (!/^\d+$/.test(portToken)) continue;
|
|
39
|
+
const port = Number(portToken);
|
|
40
|
+
if (port >= 1 && port <= 65535) {
|
|
41
|
+
lastPort = port;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lastPort;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-type fallback matching generated Dockerfile templates when EXPOSE is unavailable.
|
|
52
|
+
*
|
|
53
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
export function resolveFallbackContainerPort(config) {
|
|
57
|
+
const projectType = config.projectType || 'frontend';
|
|
58
|
+
const framework =
|
|
59
|
+
(projectType === 'both'
|
|
60
|
+
? config.backend?.framework || config.framework
|
|
61
|
+
: config.framework) ||
|
|
62
|
+
(projectType === 'frontend' ? 'react' : 'express');
|
|
63
|
+
|
|
64
|
+
if (projectType === 'frontend' && STATIC_FRONTEND_FRAMEWORKS.has(framework)) {
|
|
65
|
+
return 80;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (['laravel', 'symfony', 'php'].includes(framework)) return 80;
|
|
69
|
+
if (['fastapi', 'django', 'flask', 'python'].includes(framework)) return 8000;
|
|
70
|
+
if (['spring', 'java'].includes(framework)) return 8080;
|
|
71
|
+
if (framework === 'go') return 8080;
|
|
72
|
+
if (framework === 'dotnet') return 5000;
|
|
73
|
+
if (framework === 'rails') return 3000;
|
|
74
|
+
|
|
75
|
+
// nextjs, nestjs, express, and other Node backends
|
|
76
|
+
return 3000;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Precedence: Dockerfile EXPOSE → config.port / backend.port → per-type fallback.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} cwd
|
|
83
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
84
|
+
* @returns {Promise<{ port: number, source: 'expose'|'config'|'fallback' }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveContainerPort(cwd, config) {
|
|
87
|
+
const dockerfilePath = path.join(cwd, 'Dockerfile');
|
|
88
|
+
if (await fs.pathExists(dockerfilePath)) {
|
|
89
|
+
try {
|
|
90
|
+
const content = await fs.readFile(dockerfilePath, 'utf8');
|
|
91
|
+
const exposed = parseDockerfileExposePort(content);
|
|
92
|
+
if (exposed != null) {
|
|
93
|
+
return { port: exposed, source: 'expose' };
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// treat as unparseable / unreadable → continue
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (config.projectType === 'both' && config.backend?.port) {
|
|
101
|
+
return { port: Number(config.backend.port), source: 'config' };
|
|
102
|
+
}
|
|
103
|
+
if (config.port) {
|
|
104
|
+
return { port: Number(config.port), source: 'config' };
|
|
105
|
+
}
|
|
106
|
+
if (config.backend?.port) {
|
|
107
|
+
return { port: Number(config.backend.port), source: 'config' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { port: resolveFallbackContainerPort(config), source: 'fallback' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default {
|
|
114
|
+
parseDockerfileExposePort,
|
|
115
|
+
resolveFallbackContainerPort,
|
|
116
|
+
resolveContainerPort,
|
|
117
|
+
};
|