elestio 1.0.3 → 1.2.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 -21
- package/README.md +523 -279
- package/bin/elestio.js +5 -5
- package/package.json +49 -42
- package/src/api.js +143 -105
- package/src/cli.js +32 -838
- package/src/commands/access.js +224 -132
- package/src/commands/actions.js +427 -427
- package/src/commands/auth.js +146 -146
- package/src/commands/backups.js +215 -215
- package/src/commands/billing.js +106 -106
- package/src/commands/cicd-template.js +363 -0
- package/src/commands/cicd.js +509 -486
- package/src/commands/clusters.js +589 -0
- package/src/commands/projects.js +145 -145
- package/src/commands/services.js +306 -294
- package/src/commands/templates.js +188 -188
- package/src/commands/volumes.js +117 -117
- package/src/config.js +104 -84
- package/src/constants.js +76 -0
- package/src/deployment.js +33 -0
- package/src/payloads/pipeline.js +191 -0
- package/src/payloads/server.js +130 -0
- package/src/registry.js +715 -0
- package/src/router.js +188 -0
- package/src/templates/elestio-config.js +188 -0
- package/src/utils.js +174 -162
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { apiRequest } from '../api.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { log, colors, outputJson, formatTable } from '../utils.js';
|
|
4
|
+
import { getTemplates } from './templates.js';
|
|
5
|
+
import { createSubstitutions, normalizeElestioConfig, repoFileMounts } from '../templates/elestio-config.js';
|
|
6
|
+
import { buildGitPipelinePayload, buildComposePipelinePayload } from '../payloads/pipeline.js';
|
|
7
|
+
import { TEMPLATE_REPO_OWNER, TEMPLATE_REPO_HOST } from '../constants.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `elestio cicd deploy-template` - deploy catalog software as a CI/CD pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Every catalog entry has a matching repo under github.com/elestio-examples
|
|
13
|
+
* carrying an elestio.yml that describes how to run it: ports, environment
|
|
14
|
+
* variables, lifecycle hooks. Creating a pipeline without reading that file
|
|
15
|
+
* produces a pipeline that starts nothing, which is why this is a dedicated
|
|
16
|
+
* command rather than a flag on `cicd create`.
|
|
17
|
+
*
|
|
18
|
+
* Two routes:
|
|
19
|
+
* git - generate the template repo into the user's own Git account, then
|
|
20
|
+
* point the pipeline at it. Keeps the lifecycle scripts.
|
|
21
|
+
* compose - inline the template's docker-compose.yml. No Git account needed,
|
|
22
|
+
* but lifecycle scripts are lost because there is no checkout.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function templateRepoUrl(repoName) {
|
|
26
|
+
return `${TEMPLATE_REPO_HOST}/${TEMPLATE_REPO_OWNER}/${repoName}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Repos under elestio-examples are named after the catalog title, lowercased
|
|
31
|
+
* and hyphenated -- "N8N" -> n8n, "InvenTree" -> inventree. GitHub resolves
|
|
32
|
+
* repository names case-insensitively, so the casing does not have to match.
|
|
33
|
+
*
|
|
34
|
+
* Do not derive this from dockerhub_image: that is the upstream image
|
|
35
|
+
* ("n8nio/n8n"), not the template repo.
|
|
36
|
+
*/
|
|
37
|
+
export function templateRepoName(template) {
|
|
38
|
+
return String(template.title)
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
41
|
+
.replace(/^-+|-+$/g, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchGitContent(url, branch, file, projectId, authID = null) {
|
|
45
|
+
const response = await apiRequest('/api/cicd/getGitContent', 'POST', {
|
|
46
|
+
url, branch, file,
|
|
47
|
+
unAuthorized: !authID,
|
|
48
|
+
authID: authID || '',
|
|
49
|
+
projectID: String(projectId),
|
|
50
|
+
pipelineID: null,
|
|
51
|
+
contentPath: ''
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (response && response.status === 'KO') return null;
|
|
55
|
+
return response;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads elestio.yml from a template repo. The backend parses the YAML, so the
|
|
60
|
+
* CLI stays dependency-free.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchElestioConfig(repoUrl, branch, projectId, email, authID = null) {
|
|
63
|
+
const raw = await fetchGitContent(repoUrl, branch, 'elestio', projectId, authID);
|
|
64
|
+
const subs = createSubstitutions(email);
|
|
65
|
+
return { config: normalizeElestioConfig(raw, subs), subs, raw };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function fetchCompose(repoUrl, branch, projectId, authID = null) {
|
|
69
|
+
const response = await fetchGitContent(repoUrl, branch, 'docker-compose', projectId, authID);
|
|
70
|
+
const compose = response?.data?.dockerCompose;
|
|
71
|
+
if (!compose) {
|
|
72
|
+
throw new Error(`No docker-compose file found in ${repoUrl} on branch "${branch}"`);
|
|
73
|
+
}
|
|
74
|
+
return compose;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function resolveCicdTarget(vmID, projectId) {
|
|
78
|
+
const response = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(projectId) });
|
|
79
|
+
const services = Array.isArray(response) ? response : (response.data?.services || []);
|
|
80
|
+
const target = services.find(s => String(s.providerServerID) === String(vmID) || String(s.vmID) === String(vmID));
|
|
81
|
+
|
|
82
|
+
if (!target) {
|
|
83
|
+
const available = services.map(s => s.providerServerID || s.vmID).join(', ') || 'none';
|
|
84
|
+
throw new Error(
|
|
85
|
+
`CI/CD target ${vmID} not found in project ${projectId}. Available targets: ${available}. ` +
|
|
86
|
+
'Create one with: elestio deploy CI-CD-Target'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
displayName: target.displayName || target.name,
|
|
92
|
+
id: target.id || target.serverID,
|
|
93
|
+
serverName: target.serverName || '',
|
|
94
|
+
vmID: String(target.providerServerID || target.vmID),
|
|
95
|
+
vmProvider: target.vmProvider || target.provider || '',
|
|
96
|
+
vmRegion: target.vmRegion || target.datacenter || '',
|
|
97
|
+
levelName: target.levelName || 'Elestio-services',
|
|
98
|
+
projectID: String(projectId)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function findGitAuth(gitType, projectId) {
|
|
103
|
+
const response = await apiRequest('/api/cicd/getCICDUserAuthAccount', 'POST', {});
|
|
104
|
+
const accounts = response.data?.accounts || response.data || response.accounts || [];
|
|
105
|
+
if (!Array.isArray(accounts)) return null;
|
|
106
|
+
const match = accounts.find(a => (a.externalProviderName || a.gitType) === gitType);
|
|
107
|
+
return match ? String(match.id || match.authID) : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generates the template repo into the user's Git account.
|
|
112
|
+
* GitHub uses the template-generate API; GitLab imports the repo by URL.
|
|
113
|
+
*
|
|
114
|
+
* NOTE: /api/cicd/createRepoByTemplate currently returns 404 in production -
|
|
115
|
+
* the controller exists in the backend but is not registered in apiconfig.json,
|
|
116
|
+
* which is the route whitelist. Until that is fixed the Git route cannot work,
|
|
117
|
+
* so --no-git is the default and this path reports the gap precisely rather
|
|
118
|
+
* than surfacing a bare 404.
|
|
119
|
+
*/
|
|
120
|
+
async function generateRepoFromTemplate({ gitType, authID, owner, repoName, templateName, isPrivate, isNonOrg }) {
|
|
121
|
+
let response;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
response = await apiRequest('/api/cicd/createRepoByTemplate', 'POST', {
|
|
125
|
+
ownerName: owner,
|
|
126
|
+
repoName,
|
|
127
|
+
templateName,
|
|
128
|
+
gitType,
|
|
129
|
+
authID: String(authID),
|
|
130
|
+
isPrivate: String(!!isPrivate),
|
|
131
|
+
isNonOrg: !!isNonOrg
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (/non-JSON response \(HTTP 404\)|HTTP 404/.test(err.message)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'The Elestio API does not currently expose /api/cicd/createRepoByTemplate (404), ' +
|
|
137
|
+
'so the Git route is unavailable. Deploy with --no-git instead; the software will run, ' +
|
|
138
|
+
'but any lifecycle scripts it declares will be skipped.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (response?.status === 'KO') {
|
|
145
|
+
throw new Error(response.message || response.details?.message || `Could not create "${repoName}" in ${owner}`);
|
|
146
|
+
}
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function listDeployableTemplates(query, json = false) {
|
|
151
|
+
const templates = await getTemplates();
|
|
152
|
+
const q = (query || '').toLowerCase();
|
|
153
|
+
const matches = templates
|
|
154
|
+
.filter(t => !q || t.title?.toLowerCase().includes(q) || String(t.id) === q)
|
|
155
|
+
.map(t => ({ id: t.id, title: t.title, category: t.category, repo: templateRepoName(t) }));
|
|
156
|
+
|
|
157
|
+
if (json) { outputJson(matches); return matches; }
|
|
158
|
+
|
|
159
|
+
console.log(`\n${colors.bold}Catalog software deployable as a pipeline (${matches.length})${colors.reset}\n`);
|
|
160
|
+
console.log(formatTable(matches.slice(0, 60), [
|
|
161
|
+
{ key: 'id', label: 'ID' },
|
|
162
|
+
{ key: 'title', label: 'Software' },
|
|
163
|
+
{ key: 'category', label: 'Category' },
|
|
164
|
+
{ key: 'repo', label: 'Template repo' }
|
|
165
|
+
]));
|
|
166
|
+
if (matches.length > 60) console.log(`\n ... and ${matches.length - 60} more. Narrow with: elestio cicd templates <query>`);
|
|
167
|
+
console.log('');
|
|
168
|
+
return matches;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} nameOrId catalog template, by name or ID
|
|
173
|
+
* @param {object} options see cli.js for the flag mapping
|
|
174
|
+
*/
|
|
175
|
+
export async function deployTemplate(nameOrId, options = {}) {
|
|
176
|
+
const config = loadConfig();
|
|
177
|
+
const projectId = options.project || config.defaultProject;
|
|
178
|
+
if (!projectId) throw new Error('Project ID required. Use --project or set a default.');
|
|
179
|
+
if (!options.target) throw new Error('A CI/CD target is required (--target <vmID>). List them with: elestio cicd targets');
|
|
180
|
+
|
|
181
|
+
const templates = await getTemplates();
|
|
182
|
+
const template = templates.find(t =>
|
|
183
|
+
String(t.id) === String(nameOrId) || t.title?.toLowerCase() === String(nameOrId).toLowerCase()
|
|
184
|
+
) || templates.find(t => t.title?.toLowerCase().includes(String(nameOrId).toLowerCase()));
|
|
185
|
+
|
|
186
|
+
if (!template) {
|
|
187
|
+
throw new Error(`Template "${nameOrId}" not found. Search with: elestio cicd templates ${nameOrId}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const repoName = options.repoName || templateRepoName(template);
|
|
191
|
+
const pipelineName = options.name || repoName;
|
|
192
|
+
const branch = options.branch || 'main';
|
|
193
|
+
const sourceRepoUrl = templateRepoUrl(repoName);
|
|
194
|
+
// Opt-in, not opt-out: the Git route depends on an endpoint the API does not
|
|
195
|
+
// currently expose (see generateRepoFromTemplate).
|
|
196
|
+
const useGit = options.git === true;
|
|
197
|
+
|
|
198
|
+
log('info', `Resolving CI/CD target ${options.target}...`);
|
|
199
|
+
const target = await resolveCicdTarget(options.target, projectId);
|
|
200
|
+
log('success', `Target: ${target.displayName} (${target.vmID})`);
|
|
201
|
+
|
|
202
|
+
log('info', `Reading elestio.yml from ${TEMPLATE_REPO_OWNER}/${repoName}...`);
|
|
203
|
+
const { config: elestioConfig, subs } = await fetchElestioConfig(sourceRepoUrl, branch, projectId, config.email);
|
|
204
|
+
|
|
205
|
+
if (!elestioConfig.hasConfig) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`No elestio.yml found at ${sourceRepoUrl} (branch "${branch}"). ` +
|
|
208
|
+
`"${template.title}" may not be publishable as a pipeline; deploy it as a managed service instead: elestio deploy ${template.id}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
log('success', `Config loaded: runtime "${elestioConfig.config.runTime}", ${elestioConfig.variables ? elestioConfig.variables.split('\n').length : 0} env vars, ${elestioConfig.ports.length} port(s)`);
|
|
212
|
+
|
|
213
|
+
const payload = useGit
|
|
214
|
+
? await buildGitRoute({ options, projectId, target, template, repoName, pipelineName, branch, elestioConfig })
|
|
215
|
+
: await buildComposeRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig, force: options.force });
|
|
216
|
+
|
|
217
|
+
if (options.dryRun) {
|
|
218
|
+
if (options.json) { outputJson({ dryRun: true, route: useGit ? 'git' : 'compose', payload }); return payload; }
|
|
219
|
+
printPlan({ template, repoName, pipelineName, target, elestioConfig, useGit, branch });
|
|
220
|
+
log('info', 'To deploy, run the same command without --dry-run');
|
|
221
|
+
return payload;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
log('info', 'Creating pipeline...');
|
|
225
|
+
const response = await apiRequest('/api/cicd/createCiCdExistServer', 'POST', payload);
|
|
226
|
+
|
|
227
|
+
if (response.status !== 'OK' && !response.providerServerID && !response.pipelineID) {
|
|
228
|
+
throw new Error(response.message || JSON.stringify(response));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
log('success', `Pipeline "${pipelineName}" created on ${target.displayName}`);
|
|
232
|
+
|
|
233
|
+
if (options.json) {
|
|
234
|
+
outputJson({ status: 'OK', pipelineName, target: target.vmID, route: useGit ? 'git' : 'compose', webUI: elestioConfig.webUI, response });
|
|
235
|
+
return response;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
printAccess(elestioConfig, subs, target);
|
|
239
|
+
return response;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function buildGitRoute({ options, projectId, target, template, repoName, pipelineName, branch, elestioConfig }) {
|
|
243
|
+
const gitType = (options.gitType || 'GITHUB').toUpperCase();
|
|
244
|
+
|
|
245
|
+
let authID = options.authId ? String(options.authId) : await findGitAuth(gitType, projectId);
|
|
246
|
+
if (!authID) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`No ${gitType} account connected. Connect one in the dashboard (CI/CD > Git accounts), pass --auth-id, ` +
|
|
249
|
+
'or deploy without Git using --no-git (lifecycle scripts will be skipped).'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const owner = options.owner;
|
|
254
|
+
if (!owner) {
|
|
255
|
+
throw new Error('The Git account or organisation to create the repo in is required (--owner <user-or-org>)');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
log('info', `Creating ${owner}/${repoName} from ${TEMPLATE_REPO_OWNER}/${repoName}...`);
|
|
259
|
+
const repo = await generateRepoFromTemplate({
|
|
260
|
+
gitType, authID, owner, repoName,
|
|
261
|
+
templateName: repoName,
|
|
262
|
+
isPrivate: !!options.private,
|
|
263
|
+
isNonOrg: !!options.nonOrg
|
|
264
|
+
});
|
|
265
|
+
log('success', `Repo created: ${owner}/${repoName}`);
|
|
266
|
+
|
|
267
|
+
return buildGitPipelinePayload({
|
|
268
|
+
target,
|
|
269
|
+
projectId,
|
|
270
|
+
pipelineName,
|
|
271
|
+
gitType,
|
|
272
|
+
repo: `${owner}/${repoName}`,
|
|
273
|
+
branch,
|
|
274
|
+
repoID: repo?.id ?? repo?.content?.id ?? '',
|
|
275
|
+
elestioConfig,
|
|
276
|
+
authID,
|
|
277
|
+
isPublicGitRepo: !options.private,
|
|
278
|
+
isNeedToCreateRepo: true,
|
|
279
|
+
appType: 'docker',
|
|
280
|
+
overrides: collectOverrides(options)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function buildComposeRoute({ projectId, target, repoName, pipelineName, branch, sourceRepoUrl, elestioConfig, force }) {
|
|
285
|
+
log('info', `Fetching docker-compose from ${TEMPLATE_REPO_OWNER}/${repoName}...`);
|
|
286
|
+
const compose = await fetchCompose(sourceRepoUrl, branch, projectId);
|
|
287
|
+
log('success', 'Compose file loaded');
|
|
288
|
+
|
|
289
|
+
// Checked before anything is created: without these files the containers
|
|
290
|
+
// fail ~40s into the build with an opaque "not a directory" from runc.
|
|
291
|
+
const fileMounts = repoFileMounts(compose);
|
|
292
|
+
if (fileMounts.length > 0 && !force) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`"${repoName}" bind-mounts ${fileMounts.length} file(s) that live in the template repo ` +
|
|
295
|
+
`(${fileMounts.join(', ')}). The compose route has no checkout, so Docker creates each one as an ` +
|
|
296
|
+
'empty directory and the container fails to start. Use the Git route (--owner <git-user>) for this ' +
|
|
297
|
+
'software, or --force to deploy anyway.'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (hasLifecycleHooks(elestioConfig)) {
|
|
302
|
+
log('warn', `"${repoName}" defines lifecycle scripts that need a repo checkout; they are skipped on the compose route.`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return buildComposePipelinePayload({
|
|
306
|
+
target, projectId, pipelineName, compose, elestioConfig
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function hasLifecycleHooks(elestioConfig) {
|
|
311
|
+
return Object.values(elestioConfig.lifeCycleCommand).some(v => v && v !== '');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function collectOverrides(options) {
|
|
315
|
+
const overrides = {};
|
|
316
|
+
if (options.buildCmd !== undefined) overrides.buildCmd = options.buildCmd;
|
|
317
|
+
if (options.runCmd !== undefined) overrides.runCmd = options.runCmd;
|
|
318
|
+
if (options.installCmd !== undefined) overrides.installCmd = options.installCmd;
|
|
319
|
+
if (options.buildDir !== undefined) overrides.buildDir = options.buildDir;
|
|
320
|
+
if (options.variables !== undefined) overrides.variables = options.variables;
|
|
321
|
+
return overrides;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function printPlan({ template, repoName, pipelineName, target, elestioConfig, useGit, branch }) {
|
|
325
|
+
console.log(`\n${colors.bold}Pipeline Preview (--dry-run)${colors.reset}\n`);
|
|
326
|
+
console.log(` Software: ${colors.cyan}${template.title}${colors.reset} (ID: ${template.id})`);
|
|
327
|
+
console.log(` Template: ${TEMPLATE_REPO_OWNER}/${repoName} @ ${branch}`);
|
|
328
|
+
console.log(` Pipeline: ${pipelineName}`);
|
|
329
|
+
console.log(` Target: ${target.displayName} (${target.vmID})`);
|
|
330
|
+
console.log(` Route: ${useGit ? 'git (repo generated in your account, lifecycle scripts kept)' : 'compose (inlined, lifecycle scripts skipped)'}`);
|
|
331
|
+
console.log(` Runtime: ${elestioConfig.config.runTime || 'N/A'}`);
|
|
332
|
+
console.log(` Ports: ${elestioConfig.ports.map(p => `${p.listeningPort}->${p.targetPort}`).join(', ') || 'default'}`);
|
|
333
|
+
console.log(` Env vars: ${elestioConfig.variables ? elestioConfig.variables.split('\n').map(v => v.split('=')[0]).join(', ') : 'none'}`);
|
|
334
|
+
if (hasLifecycleHooks(elestioConfig)) {
|
|
335
|
+
const hooks = Object.entries(elestioConfig.lifeCycleCommand).filter(([, v]) => v).map(([k]) => k);
|
|
336
|
+
console.log(` Lifecycle: ${hooks.join(', ')}`);
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function printAccess(elestioConfig, subs, target) {
|
|
342
|
+
const vars = Object.fromEntries(
|
|
343
|
+
(elestioConfig.variables || '').split('\n').filter(Boolean).map(line => {
|
|
344
|
+
const eq = line.indexOf('=');
|
|
345
|
+
return [line.slice(0, eq), line.slice(eq + 1)];
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (elestioConfig.webUI.length > 0) {
|
|
350
|
+
console.log(`\n${colors.bold}Access${colors.reset}\n`);
|
|
351
|
+
for (const ui of elestioConfig.webUI) {
|
|
352
|
+
// webUI references env vars by name, e.g. password: "[ADMIN_PASSWORD]"
|
|
353
|
+
const resolve = (v) => String(v || '').replace(/\[([A-Z0-9_]+)\]/g, (m, key) => vars[key] ?? m);
|
|
354
|
+
console.log(` ${colors.bold}${ui.label || 'Web UI'}${colors.reset}`);
|
|
355
|
+
console.log(` URL: ${colors.cyan}${resolve(ui.url)}${colors.reset}`);
|
|
356
|
+
if (ui.login) console.log(` User: ${resolve(ui.login)}`);
|
|
357
|
+
if (ui.password) console.log(` Password: ${resolve(ui.password)}`);
|
|
358
|
+
}
|
|
359
|
+
console.log(`\n ${colors.dim}[CI_CD_DOMAIN] resolves once the pipeline has deployed.${colors.reset}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log(`\n Follow the build: ${colors.cyan}elestio cicd pipelines ${target.vmID}${colors.reset}\n`);
|
|
363
|
+
}
|