@ibm-cloud/cd-tools 1.7.0 → 1.8.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/cmd/direct-transfer.js +257 -37
- package/cmd/utils/requests.js +24 -1
- package/cmd/utils/terraform.js +1 -1
- package/cmd/utils/validate.js +9 -6
- package/package.json +1 -1
package/cmd/direct-transfer.js
CHANGED
|
@@ -12,6 +12,7 @@ import axios from 'axios';
|
|
|
12
12
|
import readline from 'readline/promises';
|
|
13
13
|
import { writeFile } from 'fs/promises';
|
|
14
14
|
import { TARGET_REGIONS, SOURCE_REGIONS } from '../config.js';
|
|
15
|
+
import { getWithRetry } from './utils/requests.js';
|
|
15
16
|
|
|
16
17
|
const HTTP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default
|
|
17
18
|
|
|
@@ -55,7 +56,8 @@ class GitLabClient {
|
|
|
55
56
|
return projects;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
const projRes = await
|
|
59
|
+
const projRes = await getWithRetry(
|
|
60
|
+
this.client,
|
|
59
61
|
`/groups/${currentGroupId}/projects`,
|
|
60
62
|
{ page: projPage, per_page: 100 }
|
|
61
63
|
);
|
|
@@ -82,7 +84,8 @@ class GitLabClient {
|
|
|
82
84
|
return projects;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
const subgroupRes = await
|
|
87
|
+
const subgroupRes = await getWithRetry(
|
|
88
|
+
this.client,
|
|
86
89
|
`/groups/${currentGroupId}/subgroups`,
|
|
87
90
|
{ page: subgroupPage, per_page: 100 }
|
|
88
91
|
);
|
|
@@ -107,30 +110,6 @@ class GitLabClient {
|
|
|
107
110
|
return projects;
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
// Helper: GET with retry for flaky 5xx/520 errors (Cloudflare / origin issues)
|
|
111
|
-
async getWithRetry(path, params = {}, { retries = 3, retryDelayMs = 2000 } = {}) {
|
|
112
|
-
let lastError;
|
|
113
|
-
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
114
|
-
try {
|
|
115
|
-
return await this.client.get(path, { params });
|
|
116
|
-
} catch (error) {
|
|
117
|
-
const status = error.response?.status;
|
|
118
|
-
|
|
119
|
-
if (attempt < retries && status && status >= 500) {
|
|
120
|
-
console.warn(
|
|
121
|
-
`[WARN] GET ${path} failed with status ${status} (attempt ${attempt}/${retries}). Retrying...`
|
|
122
|
-
);
|
|
123
|
-
await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt));
|
|
124
|
-
lastError = error;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
throw error; // Non-5xx or out of retries: rethrow
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
throw lastError;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
113
|
async getGroup(groupId) {
|
|
135
114
|
const response = await this.client.get(`/groups/${groupId}`);
|
|
136
115
|
return response.data;
|
|
@@ -206,6 +185,42 @@ class GitLabClient {
|
|
|
206
185
|
throw new Error(`Bulk import API call failed: ${error.response?.status} ${error.response?.statusText} - ${JSON.stringify(error.response?.data)}`);
|
|
207
186
|
}
|
|
208
187
|
}
|
|
188
|
+
|
|
189
|
+
async getBulkImportEntitiesAll(importId, { perPage = 100, maxPages = 200 } = {}) {
|
|
190
|
+
const all = [];
|
|
191
|
+
let page = 1;
|
|
192
|
+
|
|
193
|
+
while (page <= maxPages) {
|
|
194
|
+
const resp = await getWithRetry(
|
|
195
|
+
this.client,
|
|
196
|
+
`/bulk_imports/${importId}/entities`,
|
|
197
|
+
{ page, per_page: perPage }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
all.push(...(resp.data || []));
|
|
201
|
+
|
|
202
|
+
const nextPage = Number(resp.headers?.['x-next-page'] || 0);
|
|
203
|
+
if (!nextPage) break;
|
|
204
|
+
|
|
205
|
+
page = nextPage;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return all;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getGroupByFullPath(fullPath) {
|
|
212
|
+
const encoded = encodeURIComponent(fullPath);
|
|
213
|
+
const resp = await this.client.get(`/groups/${encoded}`);
|
|
214
|
+
return resp.data;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async listBulkImports({ page = 1, perPage = 50 } = {}) {
|
|
218
|
+
const resp = await getWithRetry(this.client, `/bulk_imports`, { page, per_page: perPage });
|
|
219
|
+
return {
|
|
220
|
+
imports: resp.data || [],
|
|
221
|
+
nextPage: Number(resp.headers?.['x-next-page'] || 0),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
async function promptUser(name) {
|
|
@@ -234,7 +249,7 @@ function validateAndConvertRegion(region) {
|
|
|
234
249
|
return `https://${region}.git.cloud.ibm.com/`;
|
|
235
250
|
}
|
|
236
251
|
|
|
237
|
-
// Build a mapping of: old http_url_to_repo -> new http_url_to_repo
|
|
252
|
+
// Build a mapping of: old http_url_to_repo -> new http_url_to_repo
|
|
238
253
|
async function generateUrlMappingFile({sourceUrl, destUrl, sourceGroup, destinationGroupPath, sourceProjects}) {
|
|
239
254
|
const destBase = destUrl.endsWith('/') ? destUrl.slice(0, -1) : destUrl;
|
|
240
255
|
const urlMapping = {};
|
|
@@ -267,6 +282,177 @@ async function generateUrlMappingFile({sourceUrl, destUrl, sourceGroup, destinat
|
|
|
267
282
|
console.log(`Total mapped projects: ${sourceProjects.length}`);
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
function buildGroupImportHistoryUrl(destUrl) {
|
|
286
|
+
try {
|
|
287
|
+
return new URL('import/bulk_imports/history', destUrl).toString();
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function summarizeBulkImportProgress(entities = []) {
|
|
294
|
+
let entityTotal = 0;
|
|
295
|
+
let entityFinished = 0;
|
|
296
|
+
let entityFailed = 0;
|
|
297
|
+
|
|
298
|
+
let projectTotal = 0;
|
|
299
|
+
let projectFinished = 0;
|
|
300
|
+
let projectFailed = 0;
|
|
301
|
+
|
|
302
|
+
let lastCompleted = null;
|
|
303
|
+
let lastCompletedTs = 0;
|
|
304
|
+
|
|
305
|
+
for (const e of entities) {
|
|
306
|
+
entityTotal++;
|
|
307
|
+
|
|
308
|
+
const status = e.status;
|
|
309
|
+
const isFinished = status === 'finished';
|
|
310
|
+
const isFailed = status === 'failed';
|
|
311
|
+
|
|
312
|
+
if (isFinished) entityFinished++;
|
|
313
|
+
if (isFailed) entityFailed++;
|
|
314
|
+
|
|
315
|
+
const isProjectEntity =
|
|
316
|
+
e.source_type === 'project_entity' ||
|
|
317
|
+
e.entity_type === 'project_entity' ||
|
|
318
|
+
e.entity_type === 'project';
|
|
319
|
+
|
|
320
|
+
if (isProjectEntity) {
|
|
321
|
+
projectTotal++;
|
|
322
|
+
if (isFinished) projectFinished++;
|
|
323
|
+
if (isFailed) projectFailed++;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isFinished) {
|
|
327
|
+
const ts = new Date(e.updated_at || e.created_at || 0).getTime();
|
|
328
|
+
if (ts > lastCompletedTs) {
|
|
329
|
+
lastCompletedTs = ts;
|
|
330
|
+
lastCompleted = e;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const entityDone = entityFinished + entityFailed;
|
|
336
|
+
const entityPct = entityTotal ? Math.floor((entityDone / entityTotal) * 100) : 0;
|
|
337
|
+
|
|
338
|
+
const projectDone = projectFinished + projectFailed;
|
|
339
|
+
const projectPct = projectTotal ? Math.floor((projectDone / projectTotal) * 100) : 0;
|
|
340
|
+
|
|
341
|
+
const lastCompletedLabel = lastCompleted?.source_full_path || '';
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
entityTotal,
|
|
345
|
+
entityDone,
|
|
346
|
+
entityFailed,
|
|
347
|
+
entityPct,
|
|
348
|
+
projectTotal,
|
|
349
|
+
projectDone,
|
|
350
|
+
projectFailed,
|
|
351
|
+
projectPct,
|
|
352
|
+
lastCompletedLabel,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatBulkImportProgressLine(importStatus, summary) {
|
|
357
|
+
if (!summary || summary.entityTotal === 0) {
|
|
358
|
+
return `Import status: ${importStatus} | Progress: initializing...`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const parts = [`Import status: ${importStatus}`];
|
|
362
|
+
|
|
363
|
+
if (summary.projectTotal > 0) {
|
|
364
|
+
parts.push(`Projects: ${summary.projectDone}/${summary.projectTotal} (${summary.projectPct}%)`);
|
|
365
|
+
if (summary.projectFailed > 0) parts.push(`Project failed: ${summary.projectFailed}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
parts.push(`Entities: ${summary.entityDone}/${summary.entityTotal} (${summary.entityPct}%)`);
|
|
369
|
+
if (summary.entityFailed > 0) parts.push(`Failed: ${summary.entityFailed}`);
|
|
370
|
+
|
|
371
|
+
if (summary.lastCompletedLabel) {
|
|
372
|
+
parts.push(`Last completed: ${summary.lastCompletedLabel}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return parts.join(' | ');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildGroupUrl(base, path) {
|
|
379
|
+
try {
|
|
380
|
+
return new URL(path.replace(/^\//, ''), base).toString();
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isGroupEntity(e) {
|
|
387
|
+
return e?.source_type === 'group_entity' || e?.entity_type === 'group_entity' || e?.entity_type === 'group';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function handleBulkImportConflict({destination, destUrl, sourceGroupFullPath, destinationGroupPath, importResErr}) {
|
|
391
|
+
const historyUrl = buildGroupImportHistoryUrl(destUrl);
|
|
392
|
+
const groupUrl = buildGroupUrl(destUrl, `/groups/${destinationGroupPath}`);
|
|
393
|
+
const fallback = () => {
|
|
394
|
+
console.log(`\nDestination group already exists.`);
|
|
395
|
+
if (groupUrl) console.log(`Group: ${groupUrl}`);
|
|
396
|
+
if (historyUrl) console.log(`Group import history: ${historyUrl}`);
|
|
397
|
+
process.exit(0);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
await destination.getGroupByFullPath(destinationGroupPath);
|
|
402
|
+
} catch {
|
|
403
|
+
fallback();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const IMPORT_PAGES = 3;
|
|
408
|
+
const ENTITY_PAGES = 2;
|
|
409
|
+
|
|
410
|
+
let page = 1;
|
|
411
|
+
for (let p = 0; p < IMPORT_PAGES; p++) {
|
|
412
|
+
const { imports, nextPage } = await destination.listBulkImports({ page, perPage: 50 });
|
|
413
|
+
|
|
414
|
+
for (const bi of imports) {
|
|
415
|
+
if (!bi?.id) continue;
|
|
416
|
+
|
|
417
|
+
const status = bi.status;
|
|
418
|
+
if (!['created', 'started', 'finished'].includes(status)) continue;
|
|
419
|
+
|
|
420
|
+
const entities = await destination.getBulkImportEntitiesAll(bi.id, { perPage: 100, maxPages: ENTITY_PAGES });
|
|
421
|
+
|
|
422
|
+
const matchesThisGroup = entities.some(e =>
|
|
423
|
+
isGroupEntity(e) &&
|
|
424
|
+
e.source_full_path === sourceGroupFullPath &&
|
|
425
|
+
(e.destination_full_path === destinationGroupPath || e.destination_slug === destinationGroupPath)
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (!matchesThisGroup) continue;
|
|
429
|
+
|
|
430
|
+
if (status === 'created' || status === 'started') {
|
|
431
|
+
console.log(`\nGroup is already in migration...`);
|
|
432
|
+
console.log(`Bulk import ID: ${bi.id}`);
|
|
433
|
+
if (groupUrl) console.log(`Migrated group: ${groupUrl}`);
|
|
434
|
+
if (historyUrl) console.log(`Group import history: ${historyUrl}`);
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
console.log(`\nConflict detected: ${importResErr}`);
|
|
439
|
+
console.log(`Please specify a new group name using -n, --new-name <n> when trying again`);
|
|
440
|
+
console.log(`\nGroup already migrated.`);
|
|
441
|
+
if (groupUrl) console.log(`Migrated group: ${groupUrl}`);
|
|
442
|
+
if (historyUrl) console.log(`Group import history: ${historyUrl}`);
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!nextPage) break;
|
|
447
|
+
page = nextPage;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
fallback();
|
|
451
|
+
} catch {
|
|
452
|
+
fallback();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
270
456
|
async function directTransfer(options) {
|
|
271
457
|
const sourceUrl = validateAndConvertRegion(options.sourceRegion);
|
|
272
458
|
const destUrl = validateAndConvertRegion(options.destRegion);
|
|
@@ -324,28 +510,49 @@ async function directTransfer(options) {
|
|
|
324
510
|
console.log(`Bulk import request succeeded!`);
|
|
325
511
|
console.log(`Bulk import initiated successfully (ID: ${importRes.data?.id})`);
|
|
326
512
|
} else if (importRes.conflict) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
513
|
+
await handleBulkImportConflict({
|
|
514
|
+
destination,
|
|
515
|
+
destUrl,
|
|
516
|
+
sourceGroupFullPath: sourceGroup.full_path,
|
|
517
|
+
destinationGroupPath,
|
|
518
|
+
importResErr: importRes.error
|
|
519
|
+
});
|
|
330
520
|
}
|
|
331
521
|
} catch (error) {
|
|
332
522
|
console.log(`Bulk import request failed - ${error.message}`);
|
|
333
523
|
process.exit(0);
|
|
334
524
|
}
|
|
335
525
|
|
|
336
|
-
console.log('\nPolling bulk import status (
|
|
526
|
+
console.log('\nPolling bulk import status (adaptive: 1m→2m→3m→4m→5m, max 60 checks)...');
|
|
527
|
+
const MAX_ATTEMPTS = 60;
|
|
528
|
+
const POLLS_PER_STEP = 5;
|
|
529
|
+
const MIN_INTERVAL_MIN = 1;
|
|
530
|
+
const MAX_INTERVAL_MIN = 5;
|
|
531
|
+
|
|
337
532
|
let importStatus = 'created';
|
|
338
533
|
let attempts = 0;
|
|
339
534
|
|
|
340
|
-
while (!['finished', 'failed', 'timeout'].includes(importStatus) && attempts <
|
|
535
|
+
while (!['finished', 'failed', 'timeout'].includes(importStatus) && attempts < MAX_ATTEMPTS) {
|
|
341
536
|
if (attempts > 0) {
|
|
342
|
-
|
|
343
|
-
|
|
537
|
+
const step = Math.floor(attempts / POLLS_PER_STEP);
|
|
538
|
+
const waitMin = Math.min(MIN_INTERVAL_MIN + step, MAX_INTERVAL_MIN);
|
|
539
|
+
|
|
540
|
+
console.log(`Waiting ${waitMin} minute before next status check...`);
|
|
541
|
+
await new Promise(resolve => setTimeout(resolve, waitMin * 60000));
|
|
344
542
|
}
|
|
345
543
|
try {
|
|
346
544
|
const importDetails = await destination.getBulkImport(bulkImport.id);
|
|
347
545
|
importStatus = importDetails.status;
|
|
348
|
-
|
|
546
|
+
let progressLine;
|
|
547
|
+
try {
|
|
548
|
+
const entitiesAll = await destination.getBulkImportEntitiesAll(bulkImport.id);
|
|
549
|
+
const summary = summarizeBulkImportProgress(entitiesAll);
|
|
550
|
+
progressLine = formatBulkImportProgressLine(importStatus, summary);
|
|
551
|
+
} catch {
|
|
552
|
+
progressLine = `Import status: ${importStatus} | Progress: (unable to fetch entity details)`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(`[${new Date().toLocaleTimeString()}] ${progressLine}`);
|
|
349
556
|
|
|
350
557
|
if (importStatus === 'finished') {
|
|
351
558
|
console.log('Bulk import completed successfully!');
|
|
@@ -363,8 +570,19 @@ async function directTransfer(options) {
|
|
|
363
570
|
attempts++;
|
|
364
571
|
}
|
|
365
572
|
|
|
366
|
-
if (attempts >=
|
|
367
|
-
|
|
573
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
574
|
+
const historyUrl = buildGroupImportHistoryUrl(destUrl);
|
|
575
|
+
|
|
576
|
+
console.error('\nThe CLI has stopped polling for the GitLab bulk import.');
|
|
577
|
+
console.error('The migration itself may still be running inside GitLab — the CLI only waits for a limited time.');
|
|
578
|
+
console.error(`Last reported status for bulk import ${bulkImport.id}: ${importStatus}`);
|
|
579
|
+
|
|
580
|
+
if (historyUrl) {
|
|
581
|
+
console.error('\nYou can continue monitoring this migration in the GitLab UI.');
|
|
582
|
+
console.error(`Group import history: ${historyUrl}`);
|
|
583
|
+
} else {
|
|
584
|
+
console.error('\nYou can continue monitoring this migration from the Group import history page in the GitLab UI.');
|
|
585
|
+
}
|
|
368
586
|
process.exit(0);
|
|
369
587
|
}
|
|
370
588
|
|
|
@@ -384,6 +602,8 @@ async function directTransfer(options) {
|
|
|
384
602
|
console.log(`${e.source_type}: ${e.source_full_path} (${e.status})`);
|
|
385
603
|
});
|
|
386
604
|
}
|
|
605
|
+
const migratedGroupUrl = buildGroupUrl(destUrl, `/groups/${destinationGroupPath}`);
|
|
606
|
+
if (migratedGroupUrl) console.log(`\nMigrated group: ${migratedGroupUrl}`);
|
|
387
607
|
|
|
388
608
|
return 0;
|
|
389
609
|
} else {
|
package/cmd/utils/requests.js
CHANGED
|
@@ -477,6 +477,28 @@ async function migrateToolchainSecrets(bearer, data, region) {
|
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// GET with retry for flaky 5xx/520 errors (Cloudflare / origin issues)
|
|
481
|
+
async function getWithRetry(client, path, params = {}, { retries = 3, retryDelayMs = 2000 } = {}) {
|
|
482
|
+
let lastError;
|
|
483
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
484
|
+
try {
|
|
485
|
+
return await client.get(path, { params });
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const status = error.response?.status;
|
|
488
|
+
if (attempt < retries && status && status >= 500) {
|
|
489
|
+
console.warn(
|
|
490
|
+
`[WARN] GET ${path} failed with status ${status} (attempt ${attempt}/${retries}). Retrying...`
|
|
491
|
+
);
|
|
492
|
+
await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt));
|
|
493
|
+
lastError = error;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
throw error; // Non-5xx or out of retries: rethrow
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
throw lastError;
|
|
500
|
+
}
|
|
501
|
+
|
|
480
502
|
export {
|
|
481
503
|
getBearerToken,
|
|
482
504
|
getAccountId,
|
|
@@ -495,5 +517,6 @@ export {
|
|
|
495
517
|
deleteToolchain,
|
|
496
518
|
createTool,
|
|
497
519
|
getSmInstances,
|
|
498
|
-
migrateToolchainSecrets
|
|
520
|
+
migrateToolchainSecrets,
|
|
521
|
+
getWithRetry
|
|
499
522
|
}
|
package/cmd/utils/terraform.js
CHANGED
|
@@ -205,7 +205,7 @@ async function setupTerraformFiles({ token, srcRegion, targetRegion, targetTag,
|
|
|
205
205
|
logger.print('Please enter the new URLs for the following GRIT tool(s) (or submit empty input to skip):\n');
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
const newRepoSlug = await promptUserInput(`Old URL: ${thisUrl.slice(0, thisUrl.length - 4)}\nNew URL: ${GIT_BASE_URL || 'https://' + targetRegion + '.git.cloud.ibm.com'}
|
|
208
|
+
const newRepoSlug = await promptUserInput(`Old URL: ${thisUrl.slice(0, thisUrl.length - 4)}\nNew URL: ${GIT_BASE_URL || 'https://' + targetRegion + '.git.cloud.ibm.com'}/`, '', validateGritUrlPrompt);
|
|
209
209
|
|
|
210
210
|
if (newRepoSlug) {
|
|
211
211
|
newUrl = (GIT_BASE_URL || `https://${targetRegion}.git.cloud.ibm.com`) + `/${newRepoSlug}.git`;
|
package/cmd/utils/validate.js
CHANGED
|
@@ -405,14 +405,17 @@ async function validateOAuth(token, tools, targetRegion, skipPrompt) {
|
|
|
405
405
|
|
|
406
406
|
async function validateGritUrl(token, region, url, validateFull) {
|
|
407
407
|
if (typeof url != 'string') throw Error('Provided GRIT url is not a string');
|
|
408
|
-
let trimmed;
|
|
408
|
+
let trimmed = url.trim();
|
|
409
409
|
|
|
410
410
|
if (validateFull) {
|
|
411
411
|
const baseUrl = (GIT_BASE_URL || `https://${region}.git.cloud.ibm.com`) + '/';
|
|
412
|
-
if (!
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
if (!trimmed.startsWith(baseUrl)) throw Error('Provided full GRIT url is not valid');
|
|
413
|
+
|
|
414
|
+
if (trimmed.endsWith('.git')) {
|
|
415
|
+
trimmed = trimmed.slice(baseUrl.length, trimmed.length - '.git'.length);
|
|
416
|
+
} else {
|
|
417
|
+
trimmed = trimmed.slice(baseUrl.length);
|
|
418
|
+
}
|
|
416
419
|
}
|
|
417
420
|
|
|
418
421
|
// split into two parts, user/group/subgroup and project
|
|
@@ -468,7 +471,7 @@ async function validateGritUrl(token, region, url, validateFull) {
|
|
|
468
471
|
await getGritGroupProject(accessToken, region, urlStart, projectName);
|
|
469
472
|
return trimmed;
|
|
470
473
|
} catch {
|
|
471
|
-
throw Error(
|
|
474
|
+
throw Error(`Provided GRIT url not found: ${url}`);
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
477
|
|