@ibm-cloud/cd-tools 1.9.0 → 1.11.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/cmd/copy-toolchain.js +7 -3
- package/cmd/direct-transfer.js +19 -19
- package/cmd/export-secrets.js +1 -1
- package/cmd/utils/logger.js +1 -6
- package/cmd/utils/requests.js +23 -20
- package/cmd/utils/terraform.js +1 -3
- package/cmd/utils/validate.js +1 -8
- package/eslint.config.js +33 -0
- package/package.json +2 -1
- package/test/copy-toolchain/functionalities.test.js +13 -12
package/cmd/copy-toolchain.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Licensed Materials - Property of IBM
|
|
3
|
-
* (c) Copyright IBM Corporation 2025. All Rights Reserved.
|
|
3
|
+
* (c) Copyright IBM Corporation 2025, 2026. All Rights Reserved.
|
|
4
4
|
*
|
|
5
5
|
* Note to U.S. Government Users Restricted Rights:
|
|
6
6
|
* Use, duplication or disclosure restricted by GSA ADP Schedule
|
|
@@ -19,7 +19,7 @@ import fs from 'node:fs';
|
|
|
19
19
|
|
|
20
20
|
import { Command, Option } from 'commander';
|
|
21
21
|
|
|
22
|
-
import { parseEnvVar } from './utils/utils.js';
|
|
22
|
+
import { parseEnvVar, promptUserConfirmation } from './utils/utils.js';
|
|
23
23
|
import { logger, LOG_STAGES } from './utils/logger.js';
|
|
24
24
|
import { setTerraformEnv, initProviderFile, setupTerraformFiles, runTerraformInit, getNumResourcesPlanned, runTerraformApply, getNumResourcesCreated, getNewToolchainId } from './utils/terraform.js';
|
|
25
25
|
import { getAccountId, getBearerToken, getCdInstanceByRegion, getResourceGroups, getToolchain } from './utils/requests.js';
|
|
@@ -110,7 +110,11 @@ async function main(options) {
|
|
|
110
110
|
const accountId = await getAccountId(bearer, apiKey);
|
|
111
111
|
|
|
112
112
|
// check for continuous delivery instance in target region
|
|
113
|
-
if (!await getCdInstanceByRegion(bearer, accountId, targetRegion))
|
|
113
|
+
if (!await getCdInstanceByRegion(bearer, accountId, targetRegion)) {
|
|
114
|
+
// give users the option to bypass
|
|
115
|
+
logger.warn(`Warning! Could not find a Continuous Delivery instance in the target region '${targetRegion}' or you do not have permission to view, please create one before proceeding if one does not exist already.`, LOG_STAGES.setup);
|
|
116
|
+
await promptUserConfirmation(`Do you want to proceed anyway?`, 'yes', 'Toolchain migration cancelled.');
|
|
117
|
+
}
|
|
114
118
|
|
|
115
119
|
// check for existing .tf files in output directory
|
|
116
120
|
if (fs.existsSync(outputDir)) {
|
package/cmd/direct-transfer.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Command } from 'commander';
|
|
|
11
11
|
import axios from 'axios';
|
|
12
12
|
import readline from 'readline/promises';
|
|
13
13
|
import { writeFile } from 'fs/promises';
|
|
14
|
-
import {
|
|
14
|
+
import { SOURCE_REGIONS } from '../config.js';
|
|
15
15
|
import { getWithRetry } from './utils/requests.js';
|
|
16
16
|
|
|
17
17
|
const HTTP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default
|
|
@@ -186,26 +186,26 @@ class GitLabClient {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
async getBulkImportEntitiesAll(importId, { perPage = 100, maxPages = 200 } = {}) {
|
|
190
|
+
const all = [];
|
|
191
|
+
let page = 1;
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
while (page <= maxPages) {
|
|
194
|
+
const resp = await getWithRetry(
|
|
195
|
+
this.client,
|
|
196
|
+
`/bulk_imports/${importId}/entities`,
|
|
197
|
+
{ page, per_page: perPage }
|
|
198
|
+
);
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
all.push(...(resp.data || []));
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
const nextPage = Number(resp.headers?.['x-next-page'] || 0);
|
|
203
|
+
if (!nextPage) break;
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
page = nextPage;
|
|
206
|
+
}
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
return all;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
async getGroupByFullPath(fullPath) {
|
|
@@ -250,7 +250,7 @@ function validateAndConvertRegion(region) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
// Build a mapping of: old http_url_to_repo -> new http_url_to_repo
|
|
253
|
-
async function generateUrlMappingFile({
|
|
253
|
+
async function generateUrlMappingFile({ _, destUrl, sourceGroup, destinationGroupPath, sourceProjects }) {
|
|
254
254
|
const destBase = destUrl.endsWith('/') ? destUrl.slice(0, -1) : destUrl;
|
|
255
255
|
const urlMapping = {};
|
|
256
256
|
|
|
@@ -387,7 +387,7 @@ function isGroupEntity(e) {
|
|
|
387
387
|
return e?.source_type === 'group_entity' || e?.entity_type === 'group_entity' || e?.entity_type === 'group';
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
async function handleBulkImportConflict({destination, destUrl, sourceGroupFullPath, destinationGroupPath, importResErr}) {
|
|
390
|
+
async function handleBulkImportConflict({ destination, destUrl, sourceGroupFullPath, destinationGroupPath, importResErr }) {
|
|
391
391
|
const historyUrl = buildGroupImportHistoryUrl(destUrl);
|
|
392
392
|
const groupUrl = buildGroupUrl(destUrl, `/groups/${destinationGroupPath}`);
|
|
393
393
|
const fallback = () => {
|
|
@@ -463,7 +463,7 @@ async function directTransfer(options) {
|
|
|
463
463
|
console.log(`Fetching source group from ID: ${options.groupId}...`);
|
|
464
464
|
const sourceGroup = await source.getGroup(options.groupId);
|
|
465
465
|
|
|
466
|
-
let destinationGroupName = options.newName || sourceGroup.name;
|
|
466
|
+
// let destinationGroupName = options.newName || sourceGroup.name;
|
|
467
467
|
let destinationGroupPath = options.newName || sourceGroup.path;
|
|
468
468
|
|
|
469
469
|
const sourceProjects = await source.getGroupProjects(sourceGroup.id);
|
package/cmd/export-secrets.js
CHANGED
package/cmd/utils/logger.js
CHANGED
|
@@ -107,12 +107,7 @@ class Logger {
|
|
|
107
107
|
|
|
108
108
|
async withSpinner(asyncFn, loadingMsg, successMsg, prefix, ...args) {
|
|
109
109
|
if (this.verbosity < 1 || DISABLE_SPINNER) {
|
|
110
|
-
|
|
111
|
-
return await asyncFn(...args);
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
throw (err);
|
|
115
|
-
}
|
|
110
|
+
return await asyncFn(...args);
|
|
116
111
|
}
|
|
117
112
|
|
|
118
113
|
this.spinner = ora({
|
package/cmd/utils/requests.js
CHANGED
|
@@ -327,10 +327,11 @@ async function getGritUserProject(privToken, region, user, projectName) {
|
|
|
327
327
|
};
|
|
328
328
|
const response = await axios(options);
|
|
329
329
|
switch (response.status) {
|
|
330
|
-
case 200:
|
|
330
|
+
case 200: {
|
|
331
331
|
const found = response.data?.find((entry) => entry['path'] === projectName);
|
|
332
332
|
if (!found) throw Error('GRIT user project not found');
|
|
333
333
|
return;
|
|
334
|
+
}
|
|
334
335
|
default:
|
|
335
336
|
throw Error('Get GRIT user project failed');
|
|
336
337
|
}
|
|
@@ -348,10 +349,11 @@ async function getGritGroup(privToken, region, groupName) {
|
|
|
348
349
|
};
|
|
349
350
|
const response = await axios(options);
|
|
350
351
|
switch (response.status) {
|
|
351
|
-
case 200:
|
|
352
|
+
case 200: {
|
|
352
353
|
const found = response.data?.find((entry) => entry['full_path'] === groupName);
|
|
353
354
|
if (!found) throw Error('GRIT group not found');
|
|
354
355
|
return found['id'];
|
|
356
|
+
}
|
|
355
357
|
default:
|
|
356
358
|
throw Error('Get GRIT group failed');
|
|
357
359
|
}
|
|
@@ -370,10 +372,11 @@ async function getGritGroupProject(privToken, region, groupId, projectName) {
|
|
|
370
372
|
};
|
|
371
373
|
const response = await axios(options);
|
|
372
374
|
switch (response.status) {
|
|
373
|
-
case 200:
|
|
375
|
+
case 200: {
|
|
374
376
|
const found = response.data?.find((entry) => entry['path'] === projectName);
|
|
375
377
|
if (!found) throw Error('GRIT group project not found');
|
|
376
378
|
return;
|
|
379
|
+
}
|
|
377
380
|
default:
|
|
378
381
|
throw Error('Get GRIT group project failed');
|
|
379
382
|
}
|
|
@@ -479,24 +482,24 @@ async function migrateToolchainSecrets(bearer, data, region) {
|
|
|
479
482
|
|
|
480
483
|
// GET with retry for flaky 5xx/520 errors (Cloudflare / origin issues)
|
|
481
484
|
async function getWithRetry(client, path, params = {}, { retries = 3, retryDelayMs = 2000 } = {}) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
485
|
+
let lastError;
|
|
486
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
487
|
+
try {
|
|
488
|
+
return await client.get(path, { params });
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const status = error.response?.status;
|
|
491
|
+
if (attempt < retries && status && status >= 500) {
|
|
492
|
+
console.warn(
|
|
493
|
+
`[WARN] GET ${path} failed with status ${status} (attempt ${attempt}/${retries}). Retrying...`
|
|
494
|
+
);
|
|
495
|
+
await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt));
|
|
496
|
+
lastError = error;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
throw error; // Non-5xx or out of retries: rethrow
|
|
500
|
+
}
|
|
497
501
|
}
|
|
498
|
-
|
|
499
|
-
throw lastError;
|
|
502
|
+
throw lastError;
|
|
500
503
|
}
|
|
501
504
|
|
|
502
505
|
export {
|
package/cmd/utils/terraform.js
CHANGED
|
@@ -31,7 +31,7 @@ const writeFilePromise = promisify(fs.writeFile)
|
|
|
31
31
|
async function execPromise(command, options) {
|
|
32
32
|
try {
|
|
33
33
|
const exec = promisify(child_process.exec);
|
|
34
|
-
const { stdout,
|
|
34
|
+
const { stdout, _ } = await exec(command, options);
|
|
35
35
|
return stdout.trim();
|
|
36
36
|
} catch (err) {
|
|
37
37
|
throw new Error(`Command failed: ${command} \n${err.stderr || err.stdout}`);
|
|
@@ -419,7 +419,6 @@ async function runTerraformApply(skipTfConfirmation, outputDir, verbosity, targe
|
|
|
419
419
|
});
|
|
420
420
|
|
|
421
421
|
let stdoutData = '';
|
|
422
|
-
let stderrData = '';
|
|
423
422
|
|
|
424
423
|
child.stdout.on('data', (chunk) => {
|
|
425
424
|
const text = chunk.toString();
|
|
@@ -432,7 +431,6 @@ async function runTerraformApply(skipTfConfirmation, outputDir, verbosity, targe
|
|
|
432
431
|
|
|
433
432
|
child.stderr.on('data', (chunk) => {
|
|
434
433
|
const text = chunk.toString();
|
|
435
|
-
stderrData += text;
|
|
436
434
|
if (verbosity >= 1) {
|
|
437
435
|
process.stderr.write(text);
|
|
438
436
|
logger.dump(text);
|
package/cmd/utils/validate.js
CHANGED
|
@@ -86,8 +86,7 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
86
86
|
const toolchains = await getToolchainsByName(token, accountId, tcName);
|
|
87
87
|
|
|
88
88
|
let hasSameRegion = false;
|
|
89
|
-
let
|
|
90
|
-
let hasBoth = false;
|
|
89
|
+
let hasBoth = false; // same region and resource group
|
|
91
90
|
|
|
92
91
|
if (toolchains.length > 0) {
|
|
93
92
|
let newTcName = tcName;
|
|
@@ -100,8 +99,6 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
100
99
|
} else {
|
|
101
100
|
hasSameRegion = true;
|
|
102
101
|
}
|
|
103
|
-
} else if (tc.resource_group_id === targetResourceGroupId) {
|
|
104
|
-
hasSameResourceGroup = true;
|
|
105
102
|
}
|
|
106
103
|
});
|
|
107
104
|
|
|
@@ -117,10 +114,6 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
|
|
|
117
114
|
// soft warning of confusion
|
|
118
115
|
logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Region: ${targetRegion}`, '', true);
|
|
119
116
|
}
|
|
120
|
-
// if (hasSameResourceGroup) {
|
|
121
|
-
// // soft warning of confusion
|
|
122
|
-
// logger.warn(`\nWarning! A toolchain named '${tcName}' already exists in:\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, '', true);
|
|
123
|
-
// }
|
|
124
117
|
}
|
|
125
118
|
|
|
126
119
|
if (hasBoth || hasSameRegion) {
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import { defineConfig } from "eslint/config";
|
|
4
|
+
|
|
5
|
+
export default defineConfig([
|
|
6
|
+
{
|
|
7
|
+
files: ["**/*.{js,mjs,cjs}"],
|
|
8
|
+
ignores: ["**/test/**"],
|
|
9
|
+
rules: {
|
|
10
|
+
"no-unused-vars": ["warn", {
|
|
11
|
+
"argsIgnorePattern": "^_",
|
|
12
|
+
"varsIgnorePattern": "^_",
|
|
13
|
+
"caughtErrorsIgnorePattern": "^_"
|
|
14
|
+
}],
|
|
15
|
+
"no-useless-catch": "warn",
|
|
16
|
+
"no-case-declarations": "warn",
|
|
17
|
+
"no-useless-escape": "off",
|
|
18
|
+
"no-control-regex": "off",
|
|
19
|
+
"no-regex-spaces": "off",
|
|
20
|
+
},
|
|
21
|
+
plugins: {
|
|
22
|
+
js
|
|
23
|
+
},
|
|
24
|
+
extends: ["js/recommended"],
|
|
25
|
+
languageOptions: {
|
|
26
|
+
globals: {
|
|
27
|
+
...globals.browser,
|
|
28
|
+
process: "readonly",
|
|
29
|
+
require: "readonly",
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibm-cloud/cd-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"type": "module",
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"chai": "^6.2.0",
|
|
36
|
+
"eslint": "^9.39.2",
|
|
36
37
|
"mocha": "^11.7.4",
|
|
37
38
|
"nconf": "^0.13.0",
|
|
38
39
|
"node-pty": "^1.0.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Licensed Materials - Property of IBM
|
|
3
|
-
* (c) Copyright IBM Corporation 2025. All Rights Reserved.
|
|
3
|
+
* (c) Copyright IBM Corporation 2025, 2026. All Rights Reserved.
|
|
4
4
|
*
|
|
5
5
|
* Note to U.S. Government Users Restricted Rights:
|
|
6
6
|
* Use, duplication or disclosure restricted by GSA ADP Schedule
|
|
@@ -15,7 +15,7 @@ import { expect, assert } from 'chai';
|
|
|
15
15
|
|
|
16
16
|
import { assertPtyOutput, assertExecError, areFilesInDir, deleteCreatedToolchains, parseTcIdAndRegion } from '../utils/testUtils.js';
|
|
17
17
|
import { getBearerToken, getToolchain } from '../../cmd/utils/requests.js';
|
|
18
|
-
import { TEST_TOOLCHAINS, DEFAULT_RG_ID
|
|
18
|
+
import { TEST_TOOLCHAINS, DEFAULT_RG_ID } from '../data/test-toolchains.js';
|
|
19
19
|
import { TARGET_REGIONS } from '../../config.js';
|
|
20
20
|
|
|
21
21
|
nconf.env('__');
|
|
@@ -53,16 +53,17 @@ describe('copy-toolchain: Test functionalities', function () {
|
|
|
53
53
|
timeout: 10000
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
56
|
+
// TODO: update outdated test from when missing cd instance would fail the command
|
|
57
|
+
// {
|
|
58
|
+
// name: 'Check if CD instance exists in target region',
|
|
59
|
+
// cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TARGET_REGIONS[0]],
|
|
60
|
+
// expected: new RegExp(`Could not find a Continuous Delivery instance in the target region '${TARGET_REGIONS[0]}', please create one before proceeding.`),
|
|
61
|
+
// options: {
|
|
62
|
+
// exitCondition: `Could not find a Continuous Delivery instance in the target region '${TARGET_REGIONS[0]}', please create one before proceeding.`,
|
|
63
|
+
// timeout: 10000,
|
|
64
|
+
// env: { ...process.env, MOCK_ALL_REQUESTS: 'true', MOCK_GET_CD_INSTANCE_BY_REGION_SCENARIO: 'NOT_FOUND' }
|
|
65
|
+
// }
|
|
66
|
+
// },
|
|
66
67
|
{
|
|
67
68
|
name: 'Log file is created successfully',
|
|
68
69
|
cmd: [CLI_PATH, COMMAND, '-c', TEST_TOOLCHAINS['empty'].crn, '-r', TEST_TOOLCHAINS['empty'].region],
|