contensis-cli 1.0.0-beta.89 → 1.0.0-beta.90

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.
Files changed (43) hide show
  1. package/cli.js +3 -0
  2. package/dist/commands/dev.js.map +2 -2
  3. package/dist/localisation/en-GB.js +14 -6
  4. package/dist/localisation/en-GB.js.map +2 -2
  5. package/dist/mappers/DevInit-to-CIWorkflow.js +127 -0
  6. package/dist/mappers/DevInit-to-CIWorkflow.js.map +7 -0
  7. package/dist/mappers/DevInit-to-RolePermissions.js +54 -0
  8. package/dist/mappers/DevInit-to-RolePermissions.js.map +7 -0
  9. package/dist/mappers/DevRequests-to-RequestHanderSiteConfigYaml.js +56 -0
  10. package/dist/mappers/DevRequests-to-RequestHanderSiteConfigYaml.js.map +7 -0
  11. package/dist/models/DevService.d.js +17 -0
  12. package/dist/models/DevService.d.js.map +7 -0
  13. package/dist/services/ContensisDevService.js +57 -64
  14. package/dist/services/ContensisDevService.js.map +2 -2
  15. package/dist/services/ContensisRoleService.js +7 -12
  16. package/dist/services/ContensisRoleService.js.map +2 -2
  17. package/dist/util/diff.js +63 -0
  18. package/dist/util/diff.js.map +3 -3
  19. package/dist/util/dotenv.js +57 -0
  20. package/dist/util/dotenv.js.map +7 -0
  21. package/dist/util/git.js +8 -1
  22. package/dist/util/git.js.map +2 -2
  23. package/dist/util/logger.js +12 -5
  24. package/dist/util/logger.js.map +2 -2
  25. package/dist/util/yaml.js +45 -0
  26. package/dist/util/yaml.js.map +7 -0
  27. package/dist/version.js +1 -1
  28. package/dist/version.js.map +1 -1
  29. package/package.json +3 -1
  30. package/src/commands/dev.ts +1 -0
  31. package/src/localisation/en-GB.ts +18 -8
  32. package/src/mappers/DevInit-to-CIWorkflow.ts +150 -0
  33. package/src/mappers/DevInit-to-RolePermissions.ts +33 -0
  34. package/src/mappers/DevRequests-to-RequestHanderSiteConfigYaml.ts +44 -0
  35. package/src/models/DevService.d.ts +5 -0
  36. package/src/services/ContensisDevService.ts +66 -64
  37. package/src/services/ContensisRoleService.ts +7 -15
  38. package/src/util/diff.ts +96 -0
  39. package/src/util/dotenv.ts +37 -0
  40. package/src/util/git.ts +19 -7
  41. package/src/util/logger.ts +11 -5
  42. package/src/util/yaml.ts +13 -0
  43. package/src/version.ts +1 -1
@@ -2,18 +2,26 @@ import to from 'await-to-js';
2
2
  import { execFile, spawn } from 'child_process';
3
3
  import inquirer from 'inquirer';
4
4
  import path from 'path';
5
- import { Entry, Role } from 'contensis-management-api/lib/models';
5
+ import { parse, stringify } from 'yaml';
6
6
 
7
+ import { Role } from 'contensis-management-api/lib/models';
7
8
  import { MigrateRequest } from 'migratortron';
8
- import { stringify } from 'yaml';
9
- import ContensisCli from './ContensisCliService';
9
+
10
10
  import ContensisRole from './ContensisRoleService';
11
11
  import { OutputOptionsConstructorArg } from '~/models/CliService';
12
- import { mapSiteConfigYaml } from '~/mappers/ContensisCliService-to-RequestHanderSiteConfigYaml';
12
+ import { EnvContentsToAdd } from '~/models/DevService';
13
+ import { mapSiteConfigYaml } from '~/mappers/DevRequests-to-RequestHanderSiteConfigYaml';
14
+ import {
15
+ deployKeyRole,
16
+ devKeyRole,
17
+ } from '~/mappers/DevInit-to-RolePermissions';
13
18
  import { appRootDir, readFile, writeFile } from '~/providers/file-provider';
14
19
  import { jsonFormatter } from '~/util/json.formatter';
15
20
  import { GitHelper } from '~/util/git';
16
21
  import { findByIdOrName } from '~/util/find';
22
+ import { mergeDotEnvFileContents } from '~/util/dotenv';
23
+ import { mapCIWorkflowContent } from '~/mappers/DevInit-to-CIWorkflow';
24
+ import { diffFileContent } from '~/util/diff';
17
25
 
18
26
  class ContensisDev extends ContensisRole {
19
27
  constructor(
@@ -53,14 +61,11 @@ class ContensisDev extends ContensisRole {
53
61
 
54
62
  const devKeyName = `${git.name} development`;
55
63
  const devKeyDescription = `${git.name} [contensis-cli]`;
56
- const devKeyPermissions = { blocks: [] } as Partial<Role['permissions']>;
57
64
  let existingDevKey = apiKeyExists(devKeyName);
58
65
 
59
66
  const deployKeyName = `${git.name} deployment`;
60
67
  const deployKeyDescription = `${git.name} deploy [contensis-cli]`;
61
- const deployKeyPermissions = { blocks: ['push', 'release'] } as Partial<
62
- Role['permissions']
63
- >;
68
+
64
69
  let existingDeployKey = apiKeyExists(deployKeyName);
65
70
 
66
71
  const blockId = git.name;
@@ -97,34 +102,39 @@ class ContensisDev extends ContensisRole {
97
102
  ({ ciFileName } = await inquirer.prompt([
98
103
  {
99
104
  type: 'list',
100
- message: `Multiple GitHub workflow files found\n${log.infoText(
101
- `Tell us which GitHub workflow builds the container image after each push:`
102
- )}`,
105
+ message: messages.devinit.ciMultipleChoices(),
103
106
  name: 'ciFileName',
104
107
  choices: workflowFiles,
105
108
  default: workflowFiles.find(f => f.includes('docker')),
106
109
  },
107
110
  ]));
108
111
  log.raw('');
112
+ git.ciFileName = ciFileName;
109
113
  }
110
114
 
111
115
  log.raw(log.infoText(messages.devinit.ciDetails(ciFileName)));
112
- ylog.help(messages.devinit.ciIntro(git));
113
116
 
114
- // Confirm prompt
115
- const { confirm } = await inquirer.prompt([
116
- {
117
- type: 'confirm',
118
- message: messages.devinit.confirm(),
119
- name: 'confirm',
120
- default: false,
121
- },
122
- ]);
123
- log.raw('');
124
- if (!confirm) return;
117
+ // Look at the workflow file content and make updates
118
+ const mappedWorkflow = mapCIWorkflowContent(this, git);
119
+
120
+ log.help(messages.devinit.ciIntro(git));
121
+
122
+ if (!dryRun) {
123
+ // Confirm prompt
124
+ const { confirm } = await inquirer.prompt([
125
+ {
126
+ type: 'confirm',
127
+ message: messages.devinit.confirm(),
128
+ name: 'confirm',
129
+ default: false,
130
+ },
131
+ ]);
132
+ log.raw('');
133
+ if (!confirm) return;
134
+ }
125
135
 
126
136
  // Access token prompt
127
- const { accessToken } = await inquirer.prompt([
137
+ const { accessToken }: { accessToken: string } = await inquirer.prompt([
128
138
  {
129
139
  type: 'input',
130
140
  message: messages.devinit.accessTokenPrompt(),
@@ -167,10 +177,7 @@ class ContensisDev extends ContensisRole {
167
177
  | undefined;
168
178
  existingDevRole = await this.CreateOrUpdateRole(
169
179
  existingDevRole,
170
- devKeyName,
171
- devKeyDescription,
172
- { apiKeys: [devKeyName] },
173
- devKeyPermissions
180
+ devKeyRole(devKeyName, devKeyDescription)
174
181
  );
175
182
  checkpoint('dev key role assigned');
176
183
  log.success(messages.devinit.createDevKey(devKeyName, true));
@@ -183,10 +190,7 @@ class ContensisDev extends ContensisRole {
183
190
  ) as Role | undefined;
184
191
  existingDeployRole = await this.CreateOrUpdateRole(
185
192
  existingDeployRole,
186
- deployKeyName,
187
- deployKeyDescription,
188
- { apiKeys: [deployKeyName] },
189
- deployKeyPermissions
193
+ deployKeyRole(deployKeyName, deployKeyDescription)
190
194
  );
191
195
 
192
196
  checkpoint('deploy key role assigned');
@@ -195,42 +199,31 @@ class ContensisDev extends ContensisRole {
195
199
  }
196
200
 
197
201
  // Update or create a file called .env in project home
198
- const envContents = {
202
+ const envContentsToAdd: EnvContentsToAdd = {
199
203
  ALIAS: currentEnv,
200
204
  PROJECT: currentProject,
201
- ACCESS_TOKEN: accessToken,
202
205
  };
206
+ if (accessToken) envContentsToAdd['ACCESS_TOKEN'] = accessToken;
207
+
203
208
  const envFilePath = `${projectHome}/.env`;
204
209
  const existingEnvFile = readFile(envFilePath);
205
- const existingFileLines = (existingEnvFile || '').split('\n');
206
- const envFileLines: string[] = [];
207
-
208
- const updatedEnvKeys: string[] = [];
209
- for (const ln of existingFileLines) {
210
- let newline = '';
211
- for (const [k, v] of Object.entries(envContents))
212
- if (ln.startsWith(`${k}=`)) {
213
- newline = `${k}=${v}`;
214
- updatedEnvKeys.push(k);
215
- }
216
- envFileLines.push(newline || ln);
217
- }
218
- for (const addKey of existingFileLines
219
- .filter(
220
- efl =>
221
- !updatedEnvKeys.find(uek =>
222
- uek.startsWith(`${efl.split('=')?.[0]}=`)
223
- ) && Object.keys(envContents).find(ck => ck === efl.split('=')?.[0])
224
- )
225
- .map(fl => fl.split('=')?.[0]) as (keyof typeof envContents)[]) {
226
- envFileLines.push(`${addKey}=${envContents[addKey]}`);
227
- }
210
+ const envFileLines = mergeDotEnvFileContents(
211
+ (existingEnvFile || '').split('\n').filter(l => !!l),
212
+ envContentsToAdd
213
+ );
214
+ const envDiff = diffFileContent(
215
+ existingEnvFile || '',
216
+ envFileLines.join('\n')
217
+ );
228
218
 
229
219
  if (dryRun) {
220
+ if (envDiff) {
221
+ log.info(`updating .env file ${envFilePath}: ${envDiff}`);
222
+ log.raw('');
223
+ }
230
224
  checkpoint('skip .env file update (dry-run)');
231
- log.info(`.env file`);
232
- log.object(envFileLines);
233
225
  } else {
226
+ if (envDiff) log.info(`updating .env file ${envFilePath}`);
234
227
  writeFile(envFilePath, envFileLines.join('\n'));
235
228
  checkpoint('.env file updated');
236
229
  log.success(messages.devinit.writeEnvFile());
@@ -239,11 +232,15 @@ class ContensisDev extends ContensisRole {
239
232
 
240
233
  // Update CI file -- different for GH/GL -- create a sample one with build?
241
234
  if (dryRun) {
235
+ if (mappedWorkflow?.diff) {
236
+ log.info(`updating${ciFileName} file: ${mappedWorkflow.diff}`);
237
+ log.raw('');
238
+ }
242
239
  checkpoint('skip CI file update (dry-run)');
243
- log.info(`${ciFileName} file`);
244
- // TODO: log what we might add to the file
245
- //log.object(envFileLines);
240
+ //log.object(ciFileLines);
246
241
  } else {
242
+ if (mappedWorkflow?.diff) log.info(`updating${ciFileName} file`);
243
+ writeFile(git.ciFilePath, [].join('\n'));
247
244
  log.success(messages.devinit.writeCiFile(`./${ciFileName}`));
248
245
  log.info(
249
246
  messages.devinit.ciBlockTip(blockId, currentEnv, currentProject)
@@ -261,8 +258,13 @@ class ContensisDev extends ContensisRole {
261
258
  )
262
259
  );
263
260
 
264
- log.success(messages.devinit.success());
265
- log.help(messages.devinit.startProjectTip());
261
+ if (dryRun) {
262
+ log.success(messages.devinit.dryRun());
263
+ log.help(messages.devinit.noChanges());
264
+ } else {
265
+ log.success(messages.devinit.success());
266
+ log.help(messages.devinit.startProjectTip());
267
+ }
266
268
  }
267
269
  };
268
270
 
@@ -44,10 +44,7 @@ class ContensisRole extends ContensisCli {
44
44
 
45
45
  CreateOrUpdateRole = async (
46
46
  existingRole: Role | undefined,
47
- name: string,
48
- description: string,
49
- assignments: Role['assignments'],
50
- permissions: Role['permissions']
47
+ role: Partial<Role>
51
48
  ) => {
52
49
  const { contensis, currentEnv, messages } = this;
53
50
  if (!contensis) throw new Error('shouldnt be here');
@@ -56,24 +53,19 @@ class ContensisRole extends ContensisCli {
56
53
  // TODO: check is update needed?
57
54
  const [err, updated] = await contensis.roles.UpdateRole(existingRole.id, {
58
55
  ...existingRole,
59
- assignments,
60
- permissions,
56
+ ...role,
61
57
  });
62
58
  if (err)
63
- throw new Error(messages.roles.failedSet(currentEnv, name), {
59
+ throw new Error(messages.roles.failedSet(currentEnv, role.name), {
64
60
  cause: err,
65
61
  });
66
62
  return updated;
67
63
  } else {
68
- const [err, created] = await contensis.roles.CreateRole({
69
- name,
70
- description,
71
- enabled: true,
72
- assignments,
73
- permissions,
74
- } as Role);
64
+ const [err, created] = await contensis.roles.CreateRole(
65
+ role as Omit<Role, 'id'>
66
+ );
75
67
  if (err)
76
- throw new Error(messages.roles.failedCreate(currentEnv, name), {
68
+ throw new Error(messages.roles.failedCreate(currentEnv, role.name), {
77
69
  cause: err,
78
70
  });
79
71
 
package/src/util/diff.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import chalk from 'chalk';
2
+ import { Change, diffLines } from 'diff';
3
+ import { add } from 'lodash';
4
+
1
5
  export const diffLogStrings = (updates: string, previous: string) => {
2
6
  const lastFewLines = previous.split('\n').slice(-10);
3
7
  const incomingLines = updates.split('\n');
@@ -15,3 +19,95 @@ export const diffLogStrings = (updates: string, previous: string) => {
15
19
  // Return just the incoming lines from the position we matched
16
20
  return incomingLines.slice(differentFromPos).join('\n');
17
21
  };
22
+
23
+ export const diffFileContent = (
24
+ existingContent: string,
25
+ newContent: string
26
+ ) => {
27
+ const diff = diffLines(existingContent, newContent, { newlineIsToken: true });
28
+ const diffRanges = addDiffPositionInfo(diff);
29
+
30
+ // Create formatted output for console
31
+ const output: string[] = [];
32
+ const lnSpaceLength = Math.max(
33
+ ...diffRanges.map(d => d.startLineNumber.toString().length)
34
+ );
35
+
36
+ const lnSpaces = Array(lnSpaceLength).join(' ');
37
+
38
+ for (let i = 0; i < diffRanges.length; i++) {
39
+ const part = diffRanges[i];
40
+ if (part.added || part.removed) {
41
+ const colour = part.added ? 'green' : part.removed ? 'red' : 'grey';
42
+
43
+ if (part.value !== '\n')
44
+ output.push(
45
+ `\n${part.value
46
+ .split('\n')
47
+ .map((ln, idx) =>
48
+ ln.trim() !== ''
49
+ ? `${
50
+ part.startLineNumber ? part.startLineNumber + idx : lnSpaces
51
+ }${part.added ? '+' : part.removed ? '-' : ' '} ${chalk[
52
+ colour
53
+ ](`${ln}`)}`
54
+ : ln
55
+ )
56
+ .join('\n')}`
57
+ );
58
+ }
59
+ }
60
+
61
+ return output.join('');
62
+ // return retOutput.endsWith('\n') ? retOutput : `${retOutput}\n`;
63
+ };
64
+
65
+ const addDiffPositionInfo = (diff: Change[]) => {
66
+ const diffRanges: (Change & {
67
+ startLineNumber: number;
68
+ startColumn: number;
69
+ endLineNumber: number;
70
+ endColumn: number;
71
+ })[] = [];
72
+
73
+ let lineNumber = 0;
74
+ let column = 0;
75
+ for (let partIndex = 0; partIndex < diff.length; partIndex++) {
76
+ const part = diff[partIndex];
77
+
78
+ // // Skip any parts that aren't in `after`
79
+ // if (part.removed === true) {
80
+ // continue;
81
+ // }
82
+
83
+ const startLineNumber = lineNumber;
84
+ const startColumn = column;
85
+
86
+ // Split the part into lines. Loop throug these lines to find
87
+ // the line no. and column at the end of this part.
88
+ const substring = part.value;
89
+ const lines = substring.split('\n');
90
+ lines.forEach((line, lineIndex) => {
91
+ // The first `line` is actually just a continuation of the last line
92
+ if (lineIndex === 0) {
93
+ column += line.length;
94
+ // All other lines come after a line break.
95
+ } else if (lineIndex > 0) {
96
+ lineNumber += 1;
97
+ column = line.length;
98
+ }
99
+ });
100
+
101
+ // Save a range for all of the parts with position info added
102
+ if (part.added === true || part.removed === true) {
103
+ diffRanges.push({
104
+ startLineNumber: startLineNumber + 1,
105
+ startColumn: startColumn,
106
+ endLineNumber: lineNumber,
107
+ endColumn: column,
108
+ ...part,
109
+ });
110
+ }
111
+ }
112
+ return diffRanges;
113
+ };
@@ -0,0 +1,37 @@
1
+ import { EnvContentsToAdd } from "~/models/DevService";
2
+
3
+ export const mergeDotEnvFileContents = (
4
+ existingFileLines: string[],
5
+ envContentsToAdd: EnvContentsToAdd
6
+ ): string[] => {
7
+ const envFileLines: string[] = []; // the new .env file
8
+ if (existingFileLines.length === 0) {
9
+ // There is no env file, just create one from envContentsToAdd
10
+ envFileLines.push(
11
+ ...Object.entries(envContentsToAdd).map(([k, v]) => `${k}=${v}`)
12
+ );
13
+ } else {
14
+ const updatedEnvKeys: string[] = [];
15
+ // Find lines in env that already exist for the keys in envContentsToAdd
16
+ // update them if they exist and add them to envFileLines
17
+ for (const ln of existingFileLines) {
18
+ let newline = '';
19
+ for (const [k, v] of Object.entries(envContentsToAdd))
20
+ if (ln.startsWith(`${k}=`)) {
21
+ newline = `${k}=${v}`;
22
+ updatedEnvKeys.push(k);
23
+ }
24
+ // Ensure an updated line or other lines from the existing env file are re-added
25
+ if (newline || ln) envFileLines.push(newline || ln);
26
+ }
27
+
28
+ // Add the envContentsToAdd lines to the file that did not previously exist or had an update
29
+ for (const addKey of Object.keys(envContentsToAdd).filter(
30
+ efl =>
31
+ !updatedEnvKeys.find(uek => uek.startsWith(`${efl.split('=')?.[0]}`))
32
+ ) as (keyof typeof envContentsToAdd)[]) {
33
+ envFileLines.push(`${addKey}=${envContentsToAdd[addKey]}`);
34
+ }
35
+ }
36
+ return envFileLines;
37
+ };
package/src/util/git.ts CHANGED
@@ -14,18 +14,30 @@ export type GitTypes = hostedGitInfo.Hosts;
14
14
 
15
15
  export class GitHelper {
16
16
  private gitRepoPath: string;
17
+ private ciFile?: string;
18
+
17
19
  config = {} as GitConfig;
18
20
  info: hostedGitInfo | undefined;
19
21
  home: string | undefined;
20
22
 
23
+ set ciFileName(fileName: string) {
24
+ this.ciFile = fileName;
25
+ }
26
+
21
27
  get ciFileName() {
22
- return this.workflows
23
- ? this.type === 'github'
24
- ? this.workflows.length > 1
25
- ? '[multiple workflows]'
26
- : this.workflows?.[0]
27
- : GITLAB_CI_FILENAME
28
- : '[unknown]';
28
+ return (
29
+ this.ciFile ||
30
+ (this.workflows
31
+ ? this.type === 'github'
32
+ ? this.workflows.length > 1
33
+ ? '[multiple workflows]'
34
+ : this.workflows?.[0]
35
+ : GITLAB_CI_FILENAME
36
+ : '[unknown]')
37
+ );
38
+ }
39
+ get ciFilePath() {
40
+ return `${this.gitRepoPath}/${this.ciFileName}`;
29
41
  }
30
42
  get name() {
31
43
  return (
@@ -90,11 +90,13 @@ export class Logger {
90
90
  progress.current.interrupt(message);
91
91
  };
92
92
  static debug: LogMethod = content => {
93
- const message = `${Logger.getPrefix()} ${
94
- Logger.isUserTerminal ? chalk.bgGrey(' ⚙ ') : '[DEBUG]'
95
- } ${Logger.infoText(content)}`;
96
- if (progress.active) progress.current.interrupt(message);
97
- else console.log(message);
93
+ if (['true', '1'].includes(process.env.debug || '')) {
94
+ const message = `${Logger.getPrefix()} ${
95
+ Logger.isUserTerminal ? chalk.bgGrey(' ⚙ ') : '[DEBUG]'
96
+ } ${Logger.infoText(content)}`;
97
+ if (progress.active) progress.current.interrupt(message);
98
+ else console.log(message);
99
+ }
98
100
  };
99
101
  static json: LogJsonDepthMethod = (content, depth = 9) =>
100
102
  console.dir(deepCleaner(content), { colors: true, depth });
@@ -251,6 +253,10 @@ export const logError: LogErrorFunc = (
251
253
  ) => {
252
254
  Logger[level](msg || err.message || err?.data?.message || err.Message);
253
255
  (Array.isArray(err) ? err : [err]).map((error: AppError) => {
256
+ if (typeof error === 'string') {
257
+ Logger.raw(`${Logger.infoText(error)}\n`);
258
+ return;
259
+ }
254
260
  if ('stack' in error) Logger.raw(` ${Logger.infoText(error.stack)}\n`);
255
261
  if ('data' in error)
256
262
  Logger.raw(` ${Logger.infoText(tryStringify(error.data))}\n`);
@@ -0,0 +1,13 @@
1
+ import { validateWorkflow } from '@action-validator/core';
2
+
3
+ import { parse, parseDocument, stringify } from 'yaml';
4
+
5
+ export const parseYaml = parse;
6
+ export const parseYamlDocument = parseDocument;
7
+ export const stringifyYaml = stringify;
8
+
9
+ export const validateWorkflowYaml = (yaml: string) => {
10
+ const { actionType, errors } = validateWorkflow(yaml);
11
+ if (actionType && errors.length === 0) return true;
12
+ return errors;
13
+ };
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const LIB_VERSION = "1.0.0-beta.89";
1
+ export const LIB_VERSION = "1.0.0-beta.90";