@toptal/davinci-workflow 1.8.3 → 1.8.4-alpha-fx-2755-interactive-workflow-generation.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toptal/davinci-workflow",
3
- "version": "1.8.3",
3
+ "version": "1.8.4-alpha-fx-2755-interactive-workflow-generation.4+46405792",
4
4
  "description": "GH Workflow generator package for frontend applications",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -30,12 +30,17 @@
30
30
  "url": "https://github.com/toptal/davinci/issues"
31
31
  },
32
32
  "dependencies": {
33
- "@toptal/davinci-cli-shared": "1.5.2",
34
- "@toptal/davinci-skeleton": "7.0.0",
33
+ "@toptal/davinci-cli-shared": "1.5.3-alpha-fx-2755-interactive-workflow-generation.4+46405792",
34
+ "@toptal/davinci-monorepo": "6.5.2-alpha-fx-2755-interactive-workflow-generation.4+46405792",
35
+ "@toptal/davinci-skeleton": "7.0.1-alpha-fx-2755-interactive-workflow-generation.23+46405792",
36
+ "chalk": "^4.1.2",
37
+ "cosmiconfig": "^7.0.1",
35
38
  "fs-extra": "^9.0.1",
39
+ "inquirer": "^8.2.4",
40
+ "inquirer-table-prompt": "^0.2.1",
41
+ "js-yaml": "^4.1.0",
36
42
  "lodash.kebabcase": "^4.1.1",
37
- "ora": "^5.4.1",
38
- "chalk": "^4.1.2",
39
- "js-yaml": "^4.1.0"
40
- }
43
+ "ora": "^5.4.1"
44
+ },
45
+ "gitHead": "46405792bfc49fdb4b954b2ca9ddbb9ce8660801"
41
46
  }
@@ -1,16 +1,19 @@
1
1
  const { print } = require('@toptal/davinci-cli-shared')
2
+ const {
3
+ utils: { checkIfMonorepo }
4
+ } = require('@toptal/davinci-monorepo')
2
5
  const path = require('path')
3
6
  const fs = require('fs-extra')
4
7
  const ora = require('ora')
5
8
  const kebabCase = require('lodash.kebabcase')
6
9
  const {
7
- prettifyCommands
10
+ prettifyCommands,
8
11
  } = require('@toptal/davinci-cli-shared/src/utils/print')
9
12
 
10
13
  const { WORKFLOWS_DIR, GITHUB_DIR } = require('../constants')
11
14
  const {
12
15
  copyWorkflowFromSkeleton,
13
- getWorkflowSourceFilepath
16
+ getWorkflowSourceFilepath,
14
17
  } = require('../utils/skeleton')
15
18
  const { readYMLSecrets } = require('../utils/read-secrets')
16
19
  const generateMessageFromSecrets = require('../utils/generate-command-message')
@@ -77,7 +80,7 @@ const getWorkflowPostBuildFilename = workflowName => {
77
80
  return kebabCase(workflowName) + '.js'
78
81
  }
79
82
 
80
- const performPostBuildProcedure = async (filename, destDir) => {
83
+ const performPostBuildProcedure = async (filename, destDir, workflowName) => {
81
84
  const postBuildFilePathname = path.join(
82
85
  require.resolve('@toptal/davinci-workflow'),
83
86
  '../',
@@ -96,9 +99,13 @@ const performPostBuildProcedure = async (filename, destDir) => {
96
99
  throw new Error('post build file must export a function')
97
100
  }
98
101
 
102
+ const isMonorepo = checkIfMonorepo()
103
+
99
104
  await executePostBuild({
100
105
  print,
101
- destDir
106
+ destDir,
107
+ workflowName,
108
+ isMonorepo,
102
109
  })
103
110
  }
104
111
 
@@ -129,7 +136,8 @@ const createCommand = async workflowName => {
129
136
  // execute a possible post-build procedure
130
137
  await performPostBuildProcedure(
131
138
  getWorkflowPostBuildFilename(workflowName),
132
- workflowsDir
139
+ workflowsDir,
140
+ fileName
133
141
  )
134
142
  // read and print mandatory ENV secrets that must be installed beforehand
135
143
  await printMandatorySecretsList(fileName)
@@ -165,7 +173,7 @@ const newWorkflowCommandCreator = {
165
173
 
166
174
  return createCommand(workflowName)
167
175
  },
168
- help
176
+ help,
169
177
  }
170
178
 
171
179
  module.exports = newWorkflowCommandCreator
package/src/constants.js CHANGED
@@ -21,11 +21,11 @@ const SECRET_DEFINITION = {
21
21
  'Jenkins Build Token. Necessary to execute Jenkins job',
22
22
  // alpha-package
23
23
  NPM_TOKEN_PUBLISH: 'NPM publish access token',
24
- NPM_TOKEN_READ_ONLY: 'NPM read token'
24
+ NPM_TOKEN_READ_ONLY: 'NPM read token',
25
25
  }
26
26
 
27
27
  module.exports = {
28
28
  GITHUB_DIR,
29
29
  WORKFLOWS_DIR,
30
- SECRET_DEFINITION
30
+ SECRET_DEFINITION,
31
31
  }
package/src/index.js CHANGED
@@ -2,5 +2,5 @@ const newWorkflowCommandCreator = require('./commands/new-workflow')
2
2
 
3
3
  module.exports = {
4
4
  commands: [newWorkflowCommandCreator],
5
- newWorkflowCommand: newWorkflowCommandCreator
5
+ newWorkflowCommand: newWorkflowCommandCreator,
6
6
  }
@@ -35,7 +35,7 @@ const executePostBuild = ({ destDir }) => {
35
35
  ' — it performs build&release of a new alpha package \n',
36
36
  !isScriptExist
37
37
  ? logText('New "build:package" script was added in package.json! \n')
38
- : ''
38
+ : '',
39
39
  ].join('')
40
40
  )
41
41
  }
@@ -34,7 +34,7 @@ const executePostBuild = ({ destDir }) => {
34
34
  ' — it performs build & deploy a new temploy \n',
35
35
  !isScriptExist
36
36
  ? logText('New "build" script was added in package.json! \n')
37
- : ''
37
+ : '',
38
38
  ].join('')
39
39
  )
40
40
  }
@@ -6,7 +6,7 @@ const logText = chalk.red.bold
6
6
  const yellowBoldText = chalk.yellow.bold
7
7
  const deployWorkflows = [
8
8
  'davinci-deploy-production.yml',
9
- 'davinci-deploy-staging.yml'
9
+ 'davinci-deploy-staging.yml',
10
10
  ]
11
11
 
12
12
  const executePostBuild = async ({ destDir }) => {
@@ -53,7 +53,7 @@ const executePostBuild = async ({ destDir }) => {
53
53
  ' — it performs build & deploy to staging for the latest commit in the PR branch \n',
54
54
  !isScriptExist
55
55
  ? logText('New "build" script was added in package.json! \n')
56
- : ''
56
+ : '',
57
57
  ].join('')
58
58
  )
59
59
  }
@@ -1,10 +1,116 @@
1
+ const fs = require('fs')
1
2
  const chalk = require('chalk')
3
+ const path = require('path')
4
+ const {
5
+ utils: { getWorkspacesInfo },
6
+ } = require('@toptal/davinci-monorepo')
7
+
8
+ const filterWorkspacesByCommand = require('../utils/filter-workspaces-by-command')
9
+ const setRootEnv = require('../utils/set-root-env')
10
+ const getProgramConfig = require('../utils/get-program-config')
11
+ const { readPackageJson } = require('../utils/skeleton')
12
+ const {
13
+ promptParallelWorkersForMonorepo,
14
+ promptNumber,
15
+ promptConfirm,
16
+ } = require('../utils/prompts')
2
17
 
3
18
  const CONFLUENCE_PAGE =
4
19
  'https://toptal-core.atlassian.net/l/c/qtXKBzvs#Generated-IT-Workflow'
5
20
  const yellowBoldText = chalk.yellow.bold
6
21
 
7
- const executePostBuild = () => {
22
+ const getCoverageReportDir = config => config?.['report-dir'] ?? 'coverage'
23
+
24
+ const getEnvForMonorepo = async (destDir, commands) => {
25
+ const rootDir = path.resolve(destDir, '../../')
26
+ const workspaces = await getWorkspacesInfo()
27
+
28
+ const filteredWorkspaces = filterWorkspacesByCommand(
29
+ rootDir,
30
+ workspaces,
31
+ commands
32
+ )
33
+
34
+ if (filteredWorkspaces.length === 0) {
35
+ throw new Error(
36
+ `There are no packages to run integration tests in. Following commands were not found: ${JSON.stringify(
37
+ commands
38
+ )}`
39
+ )
40
+ }
41
+
42
+ const { command, location } = filteredWorkspaces[0]
43
+
44
+ const coverageConfig = await getProgramConfig(
45
+ 'nyc',
46
+ path.join(rootDir, location)
47
+ )
48
+
49
+ return {
50
+ PARALLEL_MATRIX: JSON.stringify(
51
+ await promptParallelWorkersForMonorepo(filteredWorkspaces)
52
+ ),
53
+ COMMAND: command,
54
+ COVERAGE_REPORT_DIR: getCoverageReportDir(coverageConfig),
55
+ }
56
+ }
57
+
58
+ const getEnvForSPA = async (destDir, commands) => {
59
+ const rootDir = path.resolve(destDir, '../../')
60
+
61
+ const pkg = readPackageJson(rootDir)
62
+ const command = Object.keys(pkg.scripts).find(script =>
63
+ commands.includes(script)
64
+ )
65
+ const coverageConfig = await getProgramConfig('nyc', path.join(rootDir))
66
+
67
+ if (!command) {
68
+ throw new Error(
69
+ `No command for integration tests was found. Searched for: ${JSON.stringify(
70
+ commands
71
+ )}`
72
+ )
73
+ }
74
+
75
+ return {
76
+ PARALLEL_GROUPS: await promptNumber(
77
+ 'Input how many parallel workers you want for integration tests'
78
+ ),
79
+ COMMAND: command,
80
+ COVERAGE_REPORT_DIR: getCoverageReportDir(coverageConfig),
81
+ }
82
+ }
83
+
84
+ const executePostBuild = async ({ destDir, workflowName, isMonorepo }) => {
85
+ const workflowPath = path.join(destDir, workflowName)
86
+ // to support backward compatibility
87
+ const supportedCommands = ['test:integration:ci', 'test:ui:ci', 'test:e2e:ci']
88
+
89
+ try {
90
+ const dynamicEnv = isMonorepo
91
+ ? await getEnvForMonorepo(destDir, supportedCommands)
92
+ : await getEnvForSPA(destDir, supportedCommands)
93
+
94
+ const printCoverage = await promptConfirm(
95
+ 'Do you want to print code coverage info in your PR? \nYour project must support code-coverage generation',
96
+ false
97
+ )
98
+
99
+ setRootEnv(
100
+ {
101
+ ...dynamicEnv,
102
+ PRINT_COVERAGE: printCoverage,
103
+ },
104
+ workflowPath
105
+ )
106
+
107
+ console.log(chalk.green('\nWorkflow has been updated.'))
108
+ } catch (e) {
109
+ fs.unlinkSync(workflowPath)
110
+ console.error(e.stack)
111
+ throw new Error(`Workflow failed to be generated: ${e.message}`)
112
+ }
113
+
8
114
  console.log(
9
115
  [
10
116
  chalk.green(
@@ -18,7 +124,7 @@ const executePostBuild = () => {
18
124
  ' to make it work in your CI\n',
19
125
  'Read more about customization here ',
20
126
  chalk.grey.bold(CONFLUENCE_PAGE),
21
- '\n'
127
+ '\n',
22
128
  ].join('')
23
129
  )
24
130
  }
@@ -0,0 +1,34 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ /**
5
+ * Return workspace packages that have specified
6
+ * command(s) in their package.json scripts
7
+ * @param {string} rootDir
8
+ * @param {Object<string, { location: string }>} workspaces
9
+ * @param {Array<string>} commands
10
+ * @return {Array<{ name: string, location: string, command: string }>}
11
+ */
12
+ const filterWorkspacesByCommand = (rootDir, workspaces, commands) => {
13
+ return Object.entries(workspaces)
14
+ .map(([name, { location }]) => {
15
+ const packageJsonPath = path.join(rootDir, location, 'package.json')
16
+
17
+ if (!fs.existsSync(packageJsonPath)) {
18
+ return false
19
+ }
20
+
21
+ const pkg = require(packageJsonPath)
22
+
23
+ return {
24
+ name,
25
+ location,
26
+ command: Object.keys(pkg.scripts).find(script =>
27
+ commands.includes(script)
28
+ ),
29
+ }
30
+ })
31
+ .filter(({ command }) => command)
32
+ }
33
+
34
+ module.exports = filterWorkspacesByCommand
@@ -16,7 +16,7 @@ const generateMessageFromSecrets = secretsList => {
16
16
  '————————————————————————————————————————————————————————————————————————————————————————————————\n',
17
17
  'The following GH secrets',
18
18
  logText('must'),
19
- 'be specified in the repo to properly execute the command: \n\n'
19
+ 'be specified in the repo to properly execute the command: \n\n',
20
20
  ].join(' ')
21
21
 
22
22
  message += secretsList.reduce((messageItem, secret) => {
@@ -0,0 +1,42 @@
1
+ const path = require('path')
2
+ const { cosmiconfig } = require('cosmiconfig')
3
+
4
+ /**
5
+ * Searches for and loads configuration for your program
6
+ * Looks for:
7
+ * - a package.json property
8
+ * - a JSON or YAML, extensionless "rc file"
9
+ * - an "rc file" with the extensions .json, .yaml, .yml, .js, or .cjs
10
+ * - a .config.js or .config.cjs CommonJS module
11
+ * @param program
12
+ * @param [defaultDir] - search up the directory tree from this folder
13
+ * @return {Promise<any|{extends}>}
14
+ */
15
+ const getProgramConfig = async (program, defaultDir) => {
16
+ const explorer = cosmiconfig(program)
17
+
18
+ const defaultConfig = await explorer.search(defaultDir)
19
+
20
+ if (!defaultConfig) {
21
+ return
22
+ }
23
+
24
+ if (!defaultConfig.config.extends) {
25
+ return defaultConfig.config
26
+ }
27
+
28
+ const extendedConfig = await explorer.load(
29
+ path.join(defaultDir, defaultConfig.config.extends)
30
+ )
31
+
32
+ if (!extendedConfig) {
33
+ return defaultConfig.config
34
+ }
35
+
36
+ return {
37
+ ...defaultConfig.config,
38
+ ...extendedConfig.config
39
+ }
40
+ }
41
+
42
+ module.exports = getProgramConfig
@@ -0,0 +1,86 @@
1
+ const inquirer = require('inquirer')
2
+
3
+ inquirer.registerPrompt('table', require('inquirer-table-prompt'))
4
+
5
+ /**
6
+ * Show table prompt with selection of parallel workers for each package
7
+ * @param {Array<{ name: string, location: string, command: string }>} packages
8
+ * @param {string} [message]
9
+ * @return {Promise<Array<{ pkg: string, location: string, parallelGroups: number }>>}
10
+ */
11
+ const promptParallelWorkersForMonorepo = async (packages, message = '') => {
12
+ const { parallelWorkers } = await inquirer.prompt([
13
+ {
14
+ type: 'table',
15
+ name: 'parallelWorkers',
16
+ message: `${
17
+ message ? message + '\n' : ''
18
+ }Here are packages that support integration tests. Select how many parallel workers you want in which package.\nIf you don't choose anything, package won't be included in integration testing`,
19
+ columns: Array.from({ length: 10 }, (_, index) => ({
20
+ name: index + 1,
21
+ })),
22
+ rows: packages.map(({ name }) => ({
23
+ name,
24
+ })),
25
+ },
26
+ ])
27
+
28
+ // At least one package needs to be selected for integration tests
29
+ // so if none is selected, show the prompt again
30
+ if (parallelWorkers.filter(Boolean).length === 0) {
31
+ return promptParallelWorkersForMonorepo(
32
+ packages,
33
+ 'At least one package need to be selected for integration tests!'
34
+ )
35
+ }
36
+
37
+ return packages
38
+ .map(({ name, location }, index) => ({
39
+ pkg: name,
40
+ location,
41
+ parallelGroups: parallelWorkers[index],
42
+ }))
43
+ .filter(({ parallelGroups }) => parallelGroups)
44
+ }
45
+
46
+ /**
47
+ * Show number prompts
48
+ * @param {string} message
49
+ * @return {Promise<number>}
50
+ */
51
+ const promptNumber = async message => {
52
+ const { numberValue } = await inquirer.prompt([
53
+ {
54
+ type: 'number',
55
+ name: 'numberValue',
56
+ message,
57
+ },
58
+ ])
59
+
60
+ return numberValue
61
+ }
62
+
63
+ /**
64
+ * Show confirmation prompt
65
+ * @param {string} message
66
+ * @param {boolean} [defaultValue]
67
+ * @return {Promise<boolean>}
68
+ */
69
+ const promptConfirm = async (message, defaultValue) => {
70
+ const { confirmValue } = await inquirer.prompt([
71
+ {
72
+ type: 'confirm',
73
+ name: 'confirmValue',
74
+ message,
75
+ default: defaultValue,
76
+ },
77
+ ])
78
+
79
+ return confirmValue
80
+ }
81
+
82
+ module.exports = {
83
+ promptParallelWorkersForMonorepo,
84
+ promptNumber,
85
+ promptConfirm,
86
+ }
@@ -38,5 +38,5 @@ const readYMLSecrets = pathname => {
38
38
 
39
39
  module.exports = {
40
40
  readSecrets,
41
- readYMLSecrets
41
+ readYMLSecrets,
42
42
  }
@@ -0,0 +1,17 @@
1
+ const yaml = require('js-yaml')
2
+ const fs = require('fs')
3
+
4
+ /**
5
+ * Set/replace root ENV variables in the workflow
6
+ * @param {Object<string, any>} env
7
+ * @param {string} workflowPath
8
+ */
9
+ const setRootEnv = (env, workflowPath) => {
10
+ const workflow = yaml.load(fs.readFileSync(workflowPath, 'utf8'))
11
+
12
+ Object.assign(workflow.env, env)
13
+
14
+ fs.writeFileSync(workflowPath, yaml.dump(workflow))
15
+ }
16
+
17
+ module.exports = setRootEnv
@@ -45,5 +45,5 @@ module.exports = {
45
45
  readPackageJson,
46
46
  writePackageJson,
47
47
  getWorkflowSourceFilepath,
48
- copyWorkflowFromSkeleton
48
+ copyWorkflowFromSkeleton,
49
49
  }
@@ -23,5 +23,5 @@ const getWorkflowsDefinitions = () => {
23
23
  }
24
24
 
25
25
  module.exports = {
26
- getWorkflowsDefinitions
26
+ getWorkflowsDefinitions,
27
27
  }
@@ -1,41 +0,0 @@
1
- {
2
- "name": "@toptal/davinci-workflow",
3
- "version": "1.8.3",
4
- "description": "GH Workflow generator package for frontend applications",
5
- "publishConfig": {
6
- "access": "public"
7
- },
8
- "keywords": [
9
- "Github Actions",
10
- "GH Workflows",
11
- "generator"
12
- ],
13
- "author": "Toptal",
14
- "homepage": "https://github.com/toptal/davinci/tree/master/packages/workflow#readme",
15
- "license": "ISC",
16
- "bin": {
17
- "davinci-workflow": "./bin/davinci-workflow.js"
18
- },
19
- "main": "./src/index.js",
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/toptal/davinci.git"
23
- },
24
- "scripts": {
25
- "build:package": "../../bin/build-package.js",
26
- "prepublishOnly": "../../bin/prepublish.js",
27
- "test": "echo \"Error: run tests from root\" && exit 1"
28
- },
29
- "bugs": {
30
- "url": "https://github.com/toptal/davinci/issues"
31
- },
32
- "dependencies": {
33
- "@toptal/davinci-cli-shared": "1.5.2",
34
- "@toptal/davinci-skeleton": "7.0.0",
35
- "fs-extra": "^9.0.1",
36
- "lodash.kebabcase": "^4.1.1",
37
- "ora": "^5.4.1",
38
- "chalk": "^4.1.2",
39
- "js-yaml": "^4.1.0"
40
- }
41
- }