@ibm-cloud/cd-tools 1.15.7 → 1.15.9

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.
@@ -368,7 +368,7 @@ async function main(options) {
368
368
 
369
369
  if (verbosity >= 1) logger.print(''); // newline for spacing
370
370
  logger.info(`Toolchain "${sourceToolchainData['name']}" from ${sourceRegion} was cloned to "${targetToolchainName ?? sourceToolchainData['name']}" in ${targetRegion} ${applyErrors ? 'with some errors' : 'successfully'}, with ${numResourcesCreated} / ${numResourcesPlanned} resources created!`, LOG_STAGES.info, true);
371
- if (hasS2SFailures) logger.warn(`One or more service-to-service auth policies could not be created, see ${outputDir}/.s2s-script-failures-${TIME_SUFFIX} for more details.`, '', true);
371
+ if (hasS2SFailures) logger.warn(`Warning! One or more service-to-service auth policies could not be created, see ${outputDir}/.s2s-script-failures-${TIME_SUFFIX} for more details.`, LOG_STAGES.info, true);
372
372
  if (newTcId) logger.info(`Cloned toolchain: https://${CLOUD_PLATFORM}/devops/toolchains/${newTcId}?env_id=ibm:yp:${targetRegion}`, LOG_STAGES.info, true);
373
373
  } else {
374
374
  logger.info(`DRY_RUN: ${dryRun}, skipping terraform apply...`, LOG_STAGES.tf);
@@ -167,7 +167,7 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
167
167
  draftErrors = err;
168
168
  });
169
169
  // above is a temp fix for errors before post-processing
170
- // empty pipeline_id and trigger_id are expected and is a known provider bug
170
+ // "Insufficient initialization blocks" error is expected
171
171
 
172
172
  let generatedFile = '';
173
173
  try {
@@ -186,7 +186,7 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
186
186
  for (const [k, v] of Object.entries(value)) {
187
187
  newTfFileObj['resource'][key] = { ...(newTfFileObj['resource'][key] ?? []), [k]: v[0] };
188
188
 
189
- // remove empty tool, which breaks jsonToTf
189
+ // remove empty tool (if it exists), which breaks jsonToTf
190
190
  try {
191
191
  if (Object.keys(newTfFileObj['resource'][key][k]['source'][0]['properties'][0]['tool'][0]).length < 1) {
192
192
  delete newTfFileObj['resource'][key][k]['source'][0]['properties'][0]['tool'];
@@ -195,7 +195,7 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
195
195
  // do nothing
196
196
  }
197
197
 
198
- // handle missing worker, which breaks terraform
198
+ // handle missing worker (if it exists), which breaks terraform
199
199
  try {
200
200
  if (newTfFileObj['resource'][key][k]['worker'][0]['id'] === null) {
201
201
  delete newTfFileObj['resource'][key][k]['worker'];
@@ -209,7 +209,7 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
209
209
  if (v2 === null) delete newTfFileObj['resource'][key][k][k2];
210
210
  }
211
211
 
212
- // ignore null values in parameters
212
+ // ignore null values (if it exists) in parameters
213
213
  try {
214
214
  if (Object.keys(v[0]['parameters'][0]).length > 0) {
215
215
  for (const [k2, v2] of Object.entries(v[0]['parameters'][0])) {
@@ -220,7 +220,7 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
220
220
  // do nothing
221
221
  }
222
222
 
223
- // ignore null values in source properties
223
+ // ignore null values (if it exists) in source properties
224
224
  try {
225
225
  if (Object.keys(v[0]['source'][0]['properties'][0]).length > 0) {
226
226
  for (const [k2, v2] of Object.entries(v[0]['source'][0]['properties'][0])) {
@@ -314,16 +314,19 @@ export async function importTerraform(token, apiKey, region, toolchainId, toolch
314
314
 
315
315
  // add repo url depends_on on second pass
316
316
  for (const [key, value] of Object.entries(generatedFileJson['resource'])) {
317
- for (const [k, _] of Object.entries(value)) {
317
+ for (const [k, v] of Object.entries(value)) {
318
318
  if (key === 'ibm_cd_tekton_pipeline_definition' || key === 'ibm_cd_tekton_pipeline_trigger') {
319
+ if (key === 'ibm_cd_tekton_pipeline_trigger' && !v['source']) continue; // skip triggers without source, which aren't tied to a repo
319
320
  try {
320
321
  const thisUrl = newTfFileObj['resource'][key][k]['source'][0]['properties'][0]['url'];
321
322
 
322
323
  if (thisUrl in repoUrlMap) {
323
324
  newTfFileObj['resource'][key][k]['depends_on'] = [`\${${repoUrlMap[thisUrl].type}.${repoUrlMap[thisUrl].name}}`];
325
+ } else {
326
+ newTfFileObj['resource'][key][k]['depends_on'] = []; // we will look for and remove these in terraform.js
324
327
  }
325
- } catch {
326
- // do nothing
328
+ } catch (err) {
329
+ logger.dump(`\n[Warning] Could not add repo URL depends_on for resource "${k}": ${err.message}`);
327
330
  }
328
331
  }
329
332
  }
@@ -62,7 +62,12 @@ class Logger {
62
62
  const level = LEVELS[type] || LEVELS.log;
63
63
  const formatted = (prefix ? this.#getFullPrefix(prefix) + ' ' : '') + `${level.color}${msg}${COLORS.reset}`;
64
64
  console[level.method](formatted);
65
- this.logStream?.write(stripAnsi((prefix ? this.#getFullPrefix(prefix) + ' ' : '') + `[${type.toUpperCase()}] ` + msg) + '\n');
65
+ if (type === 'info') {
66
+ // leave out [INFO] type prefix in log file
67
+ this.logStream?.write(stripAnsi((prefix ? this.#getFullPrefix(prefix) + ' ' : '') + msg) + '\n');
68
+ } else {
69
+ this.logStream?.write(stripAnsi((prefix ? this.#getFullPrefix(prefix) + ' ' : '') + `[${type.toUpperCase()}] ` + msg) + '\n');
70
+ }
66
71
  }
67
72
 
68
73
  info(msg, prefix = '', force = false) { if (this.verbosity >= 1 || force) this.#baseLog('info', msg, prefix); }
@@ -179,8 +179,8 @@ async function setupTerraformFiles(config) {
179
179
  try {
180
180
  newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['parameters'][0]['auth_type'] = 'oauth';
181
181
  delete newTfFileObj['resource']['ibm_cd_toolchain_tool_hostedgit'][k]['parameters'][0]['api_token'];
182
- } catch {
183
- // do nothing
182
+ } catch (err) {
183
+ logger.dump(`\n[Warning] Could not convert auth_type for hostedgit tool "${k}": ${err.message}`);
184
184
  }
185
185
 
186
186
  try {
@@ -263,17 +263,24 @@ async function setupTerraformFiles(config) {
263
263
  }
264
264
  }
265
265
 
266
- // set depends_on for references to legacy GHE integrations
267
- if (hasGHE && newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger']) {
266
+ // set depends_on for references
267
+ if (newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger']) {
268
268
  for (const [k, v] of Object.entries(newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger'])) {
269
+ if (!v['source']) continue; // skip triggers without source, which aren't tied to a repo
269
270
  try {
270
271
  const thisUrl = v['source'][0]['properties'][0]['url'];
271
- if (!v['depends_on'] && thisUrl && repoToTfName[thisUrl]) {
272
- newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger'][k]['depends_on'] = [`ibm_cd_toolchain_tool_githubconsolidated.${repoToTfName[thisUrl]}`]
272
+ if (v['depends_on']?.length === 0) {
273
+ if (thisUrl && repoToTfName[thisUrl]) {
274
+ newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger'][k]['depends_on'] = [`ibm_cd_toolchain_tool_githubconsolidated.${repoToTfName[thisUrl]}`]
275
+ } else if (thisUrl && !repoToTfName[thisUrl]) {
276
+ // warn the user if the URL is not in the repoToTfName map
277
+ logger.warn(`Warning! Could not find a matching tool integration for ${thisUrl}`, LOG_STAGES.tf, true);
278
+ logger.warn(` - for definition found in ${v['pipeline_id']?.slice(2, -9)}`, LOG_STAGES.tf, true); // slice out the terraform address
279
+ delete newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger'][k]['depends_on'];
280
+ }
273
281
  }
274
- }
275
- catch {
276
- // do nothing
282
+ } catch (err) {
283
+ logger.dump(`\n[Warning] Could not set depends_on for tekton pipeline trigger "${k}": ${err.message}`);
277
284
  }
278
285
  }
279
286
  }
@@ -281,6 +288,7 @@ async function setupTerraformFiles(config) {
281
288
  // update GRIT urls
282
289
  if (newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger']) {
283
290
  for (const [k, v] of Object.entries(newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger'])) {
291
+ if (!v['source']) continue; // skip triggers without source, which aren't tied to a repo
284
292
  try {
285
293
  const thisUrl = v['source'][0]['properties'][0]['url'];
286
294
  const newUrl = gritMapping[thisUrl];
@@ -288,26 +296,31 @@ async function setupTerraformFiles(config) {
288
296
  if (newUrl) {
289
297
  newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger'][k]['source'][0]['properties'][0]['url'] = newUrl;
290
298
  }
291
- }
292
- catch {
293
- // do nothing
299
+ } catch (err) {
300
+ logger.dump(`\n[Warning] Could not update GRIT URL for tekton pipeline trigger "${k}": ${err.message}`);
294
301
  }
295
302
  }
296
303
  }
297
304
  }
298
305
 
299
306
  if (isCompact || resourceName === 'ibm_cd_tekton_pipeline_definition') {
300
- // set depends_on for references to legacy GHE integrations
301
- if (hasGHE && newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition']) {
307
+ // set depends_on for references
308
+ if (newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition']) {
302
309
  for (const [k, v] of Object.entries(newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition'])) {
303
310
  try {
304
311
  const thisUrl = v['source'][0]['properties'][0]['url'];
305
- if (!v['depends_on'] && thisUrl && repoToTfName[thisUrl]) {
306
- newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition'][k]['depends_on'] = [`ibm_cd_toolchain_tool_githubconsolidated.${repoToTfName[thisUrl]}`]
312
+ if (v['depends_on']?.length === 0) {
313
+ if (thisUrl && repoToTfName[thisUrl]) {
314
+ newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition'][k]['depends_on'] = [`ibm_cd_toolchain_tool_githubconsolidated.${repoToTfName[thisUrl]}`]
315
+ } else if (thisUrl && !repoToTfName[thisUrl]) {
316
+ // warn the user if the URL is not in the repoToTfName map
317
+ logger.warn(`Warning! Could not find a matching tool integration for ${thisUrl}`, LOG_STAGES.tf, true);
318
+ logger.warn(` - for definition found in ${v['pipeline_id']?.slice(2, -9)}`, LOG_STAGES.tf, true); // slice out the terraform address
319
+ delete newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition'][k]['depends_on'];
320
+ }
307
321
  }
308
- }
309
- catch {
310
- // do nothing
322
+ } catch (err) {
323
+ logger.dump(`\n[Warning] Could not set depends_on for tekton pipeline definition "${k}": ${err.message}`);
311
324
  }
312
325
  }
313
326
  }
@@ -322,9 +335,8 @@ async function setupTerraformFiles(config) {
322
335
  if (newUrl) {
323
336
  newTfFileObj['resource']['ibm_cd_tekton_pipeline_definition'][k]['source'][0]['properties'][0]['url'] = newUrl;
324
337
  }
325
- }
326
- catch {
327
- // do nothing
338
+ } catch (err) {
339
+ logger.dump(`\n[Warning] Could not update GRIT URL for tekton pipeline definition "${k}": ${err.message}`);
328
340
  }
329
341
  }
330
342
  }
@@ -352,9 +364,8 @@ async function setupTerraformFiles(config) {
352
364
  newTfFileObj['resource']['ibm_cd_tekton_pipeline_property'][k]['value'] = thisValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\\\n').replace(/\r/g, '\\\\r').replace(/"/g, '\\"');
353
365
  }
354
366
  }
355
- }
356
- catch {
357
- // do nothing
367
+ } catch (err) {
368
+ logger.dump(`\n[Warning] Could not process tekton pipeline property "${k}": ${err.message}`);
358
369
  }
359
370
 
360
371
  }
@@ -382,9 +393,8 @@ async function setupTerraformFiles(config) {
382
393
  newTfFileObj['resource']['ibm_cd_tekton_pipeline_trigger_property'][k]['value'] = thisValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\\\n').replace(/\r/g, '\\\\r').replace(/"/g, '\\"');
383
394
  }
384
395
  }
385
- }
386
- catch {
387
- // do nothing
396
+ } catch (err) {
397
+ logger.dump(`\n[Warning] Could not process tekton pipeline trigger property "${k}": ${err.message}`);
388
398
  }
389
399
 
390
400
  }
@@ -526,7 +536,8 @@ function replaceDependsOn(str) {
526
536
  // get rid of the quotes
527
537
  return str.replaceAll(pattern, (match, s) => ` depends_on = \[\n ${s.slice(1, s.length - 1)}\n ]`);
528
538
  }
529
- } catch {
539
+ } catch (err) {
540
+ logger.dump(`\n[Warning] Could not replace depends_on in terraform string: ${err.message}`);
530
541
  return str;
531
542
  }
532
543
  }
@@ -549,7 +560,8 @@ function addS2sScriptToToolchainTf(str, timeSuffix) {
549
560
  // get rid of the quotes
550
561
  return str.replace(pattern, (match, s1, s2) => `resource "ibm_cd_toolchain" "${s1}" {\n${s2}${provisionerStr(s1)}\n}`);
551
562
  }
552
- } catch {
563
+ } catch (err) {
564
+ logger.dump(`\n[Warning] Could not add S2S script to toolchain terraform: ${err.message}`);
553
565
  return str;
554
566
  }
555
567
  }
@@ -31,7 +31,7 @@ export async function promptUserYesNo(question) {
31
31
 
32
32
  const answer = await rl.question(fullPrompt);
33
33
 
34
- logger.dump(fullPrompt + '\n' + answer + '\n');
34
+ logger.dump(fullPrompt + answer + '\n\n');
35
35
  rl.close();
36
36
 
37
37
  const normalized = answer.toLowerCase().trim();
@@ -56,7 +56,7 @@ export async function promptUserConfirmation(question, expectedAns, exitMsg) {
56
56
  const fullPrompt = question + `\n\nOnly '${expectedAns}' will be accepted to proceed. (Ctrl-C to abort)\n\nEnter a value: `;
57
57
  const answer = await rl.question(fullPrompt);
58
58
 
59
- logger.dump(fullPrompt + '\n' + answer + '\n');
59
+ logger.dump(fullPrompt + answer + '\n\n');
60
60
 
61
61
  if (answer.toLowerCase().trim() !== expectedAns) {
62
62
  if (exitMsg) logger.print('\n' + exitMsg);
@@ -129,7 +129,7 @@ export async function promptUserSelection(question, choices) {
129
129
 
130
130
  while (true) {
131
131
  const answer = await rl.question(promptText);
132
- logger.dump(promptText + '\n' + answer + '\n');
132
+ logger.dump(promptText + answer + '\n\n');
133
133
 
134
134
  index = parseInt(answer.trim(), 10) - 1;
135
135
 
@@ -104,7 +104,7 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
104
104
 
105
105
  if (hasBoth) {
106
106
  // warning! prompt user to cancel, rename (e.g. add a suffix) or continue
107
- logger.warn(`\nWarning! A toolchain named "${tcName}" already exists in:\n - Region: ${targetRegion}\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, '', true);
107
+ logger.warn(`Warning! A toolchain named "${tcName}" already exists in:\n - Region: ${targetRegion}\n - Resource Group: ${targetResourceGroupName} (${targetResourceGroupId})`, LOG_STAGES.setup, true);
108
108
  logger.print(''); // newline for spacing
109
109
 
110
110
  if (!skipPrompt) {
@@ -113,7 +113,7 @@ async function warnDuplicateName(token, accountId, tcName, srcRegion, targetRegi
113
113
  } else {
114
114
  if (hasSameRegion) {
115
115
  // soft warning of confusion
116
- logger.warn(`\nWarning! A toolchain named "${tcName}" already exists in:\n - Region: ${targetRegion}`, '', true);
116
+ logger.warn(`Warning! A toolchain named "${tcName}" already exists in:\n - Region: ${targetRegion}`, LOG_STAGES.setup, true);
117
117
  }
118
118
  }
119
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.15.7",
3
+ "version": "1.15.9",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",
@@ -160,10 +160,10 @@ export async function deleteCreatedToolchains(toolchainsToDelete) {
160
160
  export async function assertExecError(fullCommand, expectedMessage, options, assertionFn) {
161
161
  try {
162
162
  const output = await execCommand(fullCommand, options);
163
- logger.dump(output);
163
+ logger.dump(output + '\n');
164
164
  throw new Error('Expected command to fail but it succeeded');
165
165
  } catch (e) {
166
- logger.dump(e.message);
166
+ logger.dump(e.message + '\n');
167
167
  if (assertionFn) {
168
168
  const res = assertionFn(e.message);
169
169
  if (res instanceof Promise) await res;
@@ -178,7 +178,7 @@ export async function assertExecError(fullCommand, expectedMessage, options, ass
178
178
  export async function assertPtyOutput(fullCommand, expectedMessage, options, assertionFn) {
179
179
  try {
180
180
  const output = await runPtyProcess(fullCommand, options);
181
- logger.dump(output);
181
+ logger.dump(output + '\n');
182
182
  if (assertionFn) {
183
183
  const res = assertionFn(output);
184
184
  if (res instanceof Promise) await res;
@@ -189,7 +189,7 @@ export async function assertPtyOutput(fullCommand, expectedMessage, options, ass
189
189
  }
190
190
  return parseTcIdAndRegion(output);
191
191
  } catch (e) {
192
- logger.dump(e.message);
192
+ logger.dump(e.message + '\n');
193
193
  throw (e);
194
194
  }
195
195
  }