From 31b000e8b3d8ace1c3fb3cd6ab331ed339a4e4b5 Mon Sep 17 00:00:00 2001 From: Vadim Aleksandrov Date: Mon, 14 Mar 2022 16:38:01 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=92=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D1=80=D1=83=D0=B5=D0=BC=D1=8B=D0=B5=20GitHub=20Actions?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B8=D0=B7=20Allure=20?= =?UTF-8?q?TestOps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/run/generate-github-workflow.js | 176 ++++++++++++------ .../src/commands/generate-github-workflow.js | 23 ++- .../src/commands/generate-periodic-runs.js | 2 + modules/tools/src/utils/allure.js | 63 ++++++- modules/tools/src/utils/index.js | 26 +++ 5 files changed, 226 insertions(+), 64 deletions(-) diff --git a/modules/codecept/src/commands/run/generate-github-workflow.js b/modules/codecept/src/commands/run/generate-github-workflow.js index 0beffeb6..a1e3a4f7 100644 --- a/modules/codecept/src/commands/run/generate-github-workflow.js +++ b/modules/codecept/src/commands/run/generate-github-workflow.js @@ -11,17 +11,12 @@ const { getProjectRootDir, createWorkflow, getGitHubBrowserSecretEnv, + allureJobUidStep, + getGitHubEnv, + getGitHubEnvInputs, + allurectlWatch, } = require('@csssr/e2e-tools/utils') -function getTestFilePrettyName(testFile) { - return testFile.replace(/\.test\.[jt]s$/, '') -} - -function getJobName(testFile) { - const hash = crypto.createHash('sha256').update(testFile, 'utf8').digest('hex') - return `run-test-${hash}` -} - function generateGitHubWorkflow() { const config = getConfig() const codeceptConfig = config.tools['@csssr/e2e-tools-codecept'] @@ -35,50 +30,48 @@ function generateGitHubWorkflow() { } const githubSecretsEnv = getGitHubBrowserSecretEnv(codeceptConfig.browsers) - const testFiles = glob.sync('**/*.test.{js,ts}', { - cwd: path.join(getTestsRootDir(), 'codecept/tests'), - }) const defaultRemoteBrowser = Object.entries(codeceptConfig.browsers) .filter(([_, cfg]) => cfg.remote) .map(([browserName]) => browserName)[0] - function getTestRunJob(testFile, config) { - const name = getTestFilePrettyName(testFile) - const command = `yarn et codecept:run --browser \${{ github.event.inputs.browserName }} --test 'tests/${testFile}'` + function slackMessage(status) { + function byStatus(success, failure) { + return status === 'success' ? success : failure + } return { - name, - if: `github.event.inputs.${getJobName(testFile)} == 'true'`, - 'runs-on': ['self-hosted', 'e2e-tests'], - 'timeout-minutes': 30, - steps: [ - { - uses: 'actions/checkout@v2', - with: { - lfs: true, - }, - }, - { - run: 'yarn install --frozen-lockfile', - 'working-directory': 'e2e-tests', - }, - config.allure?.projectId && downloadAllurectlStep(), - { - run: command, - 'working-directory': 'e2e-tests', - env: { - ...githubSecretsEnv, - LAUNCH_URL: '${{ github.event.inputs.launchUrl }}', - ...(config.allure?.projectId && { ENABLE_ALLURE_REPORT: 'true' }), - ...(config.allure?.projectId && allureEnv(config, name, command)), - }, - }, - config.allure?.projectId && allurectlUploadStep(config, name, command), - ].filter(Boolean), + 'slack-bot-user-oauth-access-token': '${{ secrets.SLACK_SEND_MESSAGE_TOKEN }}', + 'slack-channel': codeceptConfig.githubActions?.slackChannel, + 'slack-text': [ + byStatus( + ':approved: *Тесты прошли успешно*', + ':fire: *${{ steps.allure.outputs.report-failed-percentage }}% (${{ steps.allure.outputs.report-failed-total }}) тестов упало*' + ), + '', + `Название: Run tests`, + 'URL: ${{ github.event.inputs.launchUrl }}', + 'Команда для запуска:', + '```', + 'yarn et codecept:run --browser ${{ github.event.inputs.browserName }}', + '```', + '', + '*Allure отчёт*: ${{ steps.allure.outputs.report-link }}', + '', + '${{ steps.allure.outputs.report-summary }}', + '', + 'Логи: https://github.com/${{ github.repository }}/runs/${{ steps.query-jobs.outputs.result }}?check_suite_focus=true', + '', + `https://s.csssr.ru/U09LGPMEU/${byStatus( + '20200731115845', + '20200731115800' + )}.jpg?run=\${{ github.run_id }}`, + ].join('\n'), } } + const name = 'Run CodeceptJS e2e tests' + const workflowContent = { name: 'Run CodeceptJS e2e tests', concurrency: 'e2e-tests', @@ -95,17 +88,13 @@ function generateGitHubWorkflow() { default: defaultRemoteBrowser, required: true, }, - - ...Object.fromEntries( - testFiles.map((testFile) => [ - getJobName(testFile), - { - description: `Запустить тест «${getTestFilePrettyName(testFile)}»`, - required: false, - default: 'true', - }, - ]) - ), + ...(codeceptConfig.allureTestOpsJobs?.enabled && { + ALLURE_JOB_RUN_ID: { + description: 'Inner parameter for Allure TestOps', + required: false, + }, + }), + ...getGitHubEnvInputs(config.env), }, }, }, @@ -121,9 +110,82 @@ function generateGitHubWorkflow() { 'security-events': 'none', statuses: 'none', }, - jobs: Object.fromEntries( - testFiles.map((testFile) => [getJobName(testFile), getTestRunJob(testFile, config)]) - ), + jobs: { + 'run-tests': { + name: 'Run tests', + 'runs-on': ['self-hosted', 'e2e-tests'], + 'timeout-minutes': 90, + steps: [ + { + uses: 'actions/checkout@v2', + with: { + lfs: true, + }, + }, + config.allure?.projectId && allureJobUidStep(), + { + run: 'yarn install --frozen-lockfile', + 'working-directory': 'e2e-tests', + }, + config.allure?.projectId && downloadAllurectlStep(), + { + run: allurectlWatch( + config, + `yarn et codecept:run --browser \${{ github.event.inputs.browserName }}` + ), + 'working-directory': 'e2e-tests', + env: { + ...getGitHubEnv(config.env), + ...githubSecretsEnv, + LAUNCH_URL: '${{ github.event.inputs.launchUrl }}', + ...(config.allure?.projectId && { ENABLE_ALLURE_REPORT: 'true' }), + ...(config.allure?.projectId && + allureEnv( + config, + `${name} in \${{ github.event.inputs.browserName }}`, + 'codecept', + codeceptConfig.allureTestOpsJobs?.enabled + )), + }, + }, + codeceptConfig.githubActions?.slackChannel && { + if: 'always()', + uses: 'actions/github-script@v4', + id: 'query-jobs', + with: { + script: [ + 'const result = await github.actions.listJobsForWorkflowRun({', + ' owner: context.repo.owner,', + ' repo: context.repo.repo,', + ' run_id: ${{ github.run_id }},', + '})', + 'return result.data.jobs[0].id', + ].join('\n'), + 'result-encoding': 'string', + }, + }, + codeceptConfig.githubActions?.slackChannel && { + if: 'failure()', + name: 'Send failure to Slack', + uses: 'archive/github-actions-slack@27663f2377ce6f86d7fca5b8056e6b977f03b5c9', + with: slackMessage('failure'), + }, + codeceptConfig.githubActions?.slackChannel && { + if: 'success()', + name: 'Send success to Slack', + uses: 'archive/github-actions-slack@27663f2377ce6f86d7fca5b8056e6b977f03b5c9', + with: slackMessage('success'), + }, + allurectlUploadStep( + config, + name, + 'codecept', + '', + codeceptConfig.allureTestOpsJobs?.enabled + ), + ].filter(Boolean), + }, + }, } createWorkflow(githubWorkflowPath, workflowContent) diff --git a/modules/nightwatch/src/commands/generate-github-workflow.js b/modules/nightwatch/src/commands/generate-github-workflow.js index c79ca53e..64446ac2 100644 --- a/modules/nightwatch/src/commands/generate-github-workflow.js +++ b/modules/nightwatch/src/commands/generate-github-workflow.js @@ -9,6 +9,9 @@ const { allurectlUploadStep, allureEnv, downloadAllurectlStep, + allureJobUidStep, + getGitHubEnv, + getGitHubEnvInputs, } = require('@csssr/e2e-tools/utils') function generateGitHubWorkflow() { @@ -87,6 +90,13 @@ function generateGitHubWorkflow() { default: 'true', required: true, }, + ...(nightwatchConfig.allureTestOpsJobs?.enabled && { + ALLURE_JOB_RUN_ID: { + description: 'Inner parameter for Allure TestOps', + required: false, + }, + }), + ...getGitHubEnvInputs(config.env), }, }, }, @@ -114,6 +124,7 @@ function generateGitHubWorkflow() { lfs: true, }, }, + config.allure?.projectId && allureJobUidStep(), { run: 'yarn install --frozen-lockfile', 'working-directory': 'e2e-tests', @@ -126,13 +137,15 @@ function generateGitHubWorkflow() { ), 'working-directory': 'e2e-tests', env: { + ...getGitHubEnv(config.env), ...githubSecretsEnv, LAUNCH_URL: '${{ github.event.inputs.launchUrl }}', ENABLE_ALLURE_REPORT: 'true', ...allureEnv( config, `${name} in \${{ github.event.inputs.browserName }}`, - 'nightwatch' + 'nightwatch', + nightwatchConfig.allureTestOpsJobs?.enabled ), }, }, @@ -180,7 +193,13 @@ function generateGitHubWorkflow() { uses: 'archive/github-actions-slack@27663f2377ce6f86d7fca5b8056e6b977f03b5c9', with: slackMessage('success'), }, - allurectlUploadStep(config, name, 'nightwatch'), + allurectlUploadStep( + config, + name, + 'nightwatch', + '', + nightwatchConfig.allureTestOpsJobs?.enabled + ), ].filter(Boolean), }, }, diff --git a/modules/tools/src/commands/generate-periodic-runs.js b/modules/tools/src/commands/generate-periodic-runs.js index a7c74d1e..4db1d4bb 100644 --- a/modules/tools/src/commands/generate-periodic-runs.js +++ b/modules/tools/src/commands/generate-periodic-runs.js @@ -12,6 +12,7 @@ const { allurectlUploadStep, allureEnv, downloadAllurectlStep, + allureJobUidStep, } = require('../utils') const templateContext = { url: '${{ github.event.deployment_status.environment_url }}', @@ -146,6 +147,7 @@ function generatePeriodicRunWorkflow({ url, command, run, id, config }) { 'timeout-minutes': 90, steps: [ ...getCheckoutSteps(run), + config.allure?.projectId && allureJobUidStep(), { run: 'yarn install --frozen-lockfile', 'working-directory': 'e2e-tests', diff --git a/modules/tools/src/utils/allure.js b/modules/tools/src/utils/allure.js index 44bcf67b..73c040ad 100644 --- a/modules/tools/src/utils/allure.js +++ b/modules/tools/src/utils/allure.js @@ -4,7 +4,7 @@ function allurectlWatch(config, command) { : command } -function allurectlUploadStep(config, name, command, type) { +function allurectlUploadStep(config, name, command, type, allureTestOpsJobs) { const allureLaunchName = type === 'periodic' ? name : `${name} in \${{ github.event.inputs.browserName }}` return ( @@ -15,7 +15,7 @@ function allurectlUploadStep(config, name, command, type) { 'working-directory': 'e2e-tests', run: `./allurectl upload ${resultsDirectory(command)}`, env: { - ...allureEnv(config, allureLaunchName, command), + ...allureEnv(config, allureLaunchName, command, allureTestOpsJobs), }, } ) @@ -25,16 +25,34 @@ function resultsDirectory(command) { return `${command.includes('nightwatch') ? 'nightwatch' : 'codecept'}/report/allure-reports/` } -function allureEnv(config, name, command) { +function allureEnv(config, name, command, allureTestOpsJobs) { + function getGitHubEnv(env) { + return Object.entries(env || {}).reduce((acc, [envName, env]) => { + return { + ...acc, + [envName]: `\${{ github.event.inputs.${envName} }}`, + } + }, {}) + } + return ( config.allure?.projectId && { ALLURE_ENDPOINT: '${{ secrets.ALLURE_ENDPOINT }}', ALLURE_TOKEN: '${{ secrets.ALLURE_TOKEN }}', ALLURE_PROJECT_ID: config.allure?.projectId, - ALLURE_JOB_UID: '${{ github.run_id }}', + ALLURE_JOB_UID: '${{steps.allure-job-uid.outputs.result}}', ALLURE_CI_TYPE: 'github', ALLURE_LAUNCH_NAME: name, ALLURE_RESULTS: resultsDirectory(command), + ...(allureTestOpsJobs && { + ALLURE_JOB_RUN_ID: '${{ github.event.inputs.ALLURE_JOB_RUN_ID }}', + launchUrl: '${{ github.event.inputs.launchUrl }}', + browserName: '${{ github.event.inputs.browserName }}', + ...(command.includes('nightwatch') && { + checkScreenshots: '${{ github.event.inputs.checkScreenshots }}', + }), + ...getGitHubEnv(config.env), + }), } ) } @@ -50,4 +68,39 @@ function downloadAllurectlStep() { } } -module.exports = { allurectlWatch, allurectlUploadStep, allureEnv, downloadAllurectlStep } +function allureJobUidStep() { + function deindent(s) { + const lines = s.split('\n').filter((line, index, lines) => { + const isFirstOrLastLine = index === 0 || index === lines.length - 1 + return !(isFirstOrLastLine && line.trim() === '') + }) + + const indents = lines.filter(Boolean).map((line) => line.length - line.trimLeft().length) + const minIndent = Math.min(...indents) + const linesWithoutIndent = lines.map((line) => (line[0] === ' ' ? line.slice(minIndent) : line)) + return linesWithoutIndent.join('\n') + } + + return { + uses: 'actions/github-script@v6', + id: 'allure-job-uid', + with: { + 'result-encoding': 'string', + script: deindent(` + const result = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId, + }); + return \`\${context.repo.owner}/\${context.repo.repo}/actions/workflows/\${result.data.workflow_id}\``), + }, + } +} + +module.exports = { + allurectlWatch, + allurectlUploadStep, + allureEnv, + downloadAllurectlStep, + allureJobUidStep, +} diff --git a/modules/tools/src/utils/index.js b/modules/tools/src/utils/index.js index 4221e506..e0dbe1fc 100644 --- a/modules/tools/src/utils/index.js +++ b/modules/tools/src/utils/index.js @@ -16,6 +16,7 @@ const { allurectlUploadStep, allureEnv, downloadAllurectlStep, + allureJobUidStep, } = require('./allure') const getTestsRootDir = () => { @@ -327,6 +328,28 @@ function getGitHubSecretEnv(env) { }, {}) } +function getGitHubEnv(env) { + return Object.entries(env || {}).reduce((acc, [envName, env]) => { + return { + ...acc, + [envName]: `\${{ github.event.inputs.${envName} }}`, + } + }, {}) +} + +function getGitHubEnvInputs(env) { + return Object.entries(env || {}).reduce((acc, [envName, env]) => { + return { + ...acc, + [envName]: { + description: env.description, + default: env.default, + required: true, + }, + } + }, {}) +} + function getGitHubBrowserSecretEnv(browsers) { return Object.entries(browsers || {}) .filter( @@ -400,10 +423,13 @@ module.exports = { stripDirectoryNameCaseInsensitive, createWorkflow, getGitHubSecretEnv, + getGitHubEnv, + getGitHubEnvInputs, getGitHubBrowserSecretEnv, ensureNodeVersion, allurectlWatch, allurectlUploadStep, allureEnv, downloadAllurectlStep, + allureJobUidStep, }