diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index b46c124d149b..920aea0aa308 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -67,6 +67,8 @@ on: required: false CODECOV_TOKEN: required: false + REPORTER_JIRA_ROCKETCHAT_API_KEY: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -250,10 +252,15 @@ jobs: IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }} REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }} REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }} REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }} + REPORTER_ROCKETCHAT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPORTER_ROCKETCHAT_AUTHOR: ${{ github.event.pull_request.user.login }} + REPORTER_ROCKETCHAT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORTER_ROCKETCHAT_PR: ${{ github.event.pull_request.number }} QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} QASE_REPORT: ${{ github.ref == 'refs/heads/develop' && 'true' || '' }} CI: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9aa8f616857..ce18d17b8b80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,6 +349,7 @@ jobs: QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} test-api-ee: name: 🔨 Test API (EE) @@ -400,6 +401,7 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} test-ui-ee-no-watcher: name: 🔨 Test UI (EE) @@ -430,6 +432,7 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} tests-done: name: ✅ Tests Done diff --git a/apps/meteor/playwright.config.ts b/apps/meteor/playwright.config.ts index d40592b8f71f..822f78e28741 100644 --- a/apps/meteor/playwright.config.ts +++ b/apps/meteor/playwright.config.ts @@ -30,6 +30,21 @@ export default { branch: process.env.REPORTER_ROCKETCHAT_BRANCH, run: Number(process.env.REPORTER_ROCKETCHAT_RUN), draft: process.env.REPORTER_ROCKETCHAT_DRAFT === 'true', + headSha: process.env.REPORTER_ROCKETCHAT_HEAD_SHA, + }, + ], + process.env.REPORTER_ROCKETCHAT_REPORT === 'true' && [ + './reporters/jira.ts', + { + url: `https://rocketchat.atlassian.net`, + apiKey: process.env.REPORTER_JIRA_ROCKETCHAT_API_KEY ?? process.env.JIRA_TOKEN, + branch: process.env.REPORTER_ROCKETCHAT_BRANCH, + run: Number(process.env.REPORTER_ROCKETCHAT_RUN), + headSha: process.env.REPORTER_ROCKETCHAT_HEAD_SHA, + author: process.env.REPORTER_ROCKETCHAT_AUTHOR, + run_url: process.env.REPORTER_ROCKETCHAT_RUN_URL, + pr: Number(process.env.REPORTER_ROCKETCHAT_PR), + draft: process.env.REPORTER_ROCKETCHAT_DRAFT === 'true', }, ], [ diff --git a/apps/meteor/reporters/jira.ts b/apps/meteor/reporters/jira.ts new file mode 100644 index 000000000000..706856389003 --- /dev/null +++ b/apps/meteor/reporters/jira.ts @@ -0,0 +1,186 @@ +import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter'; +import fetch from 'node-fetch'; + +class JIRAReporter implements Reporter { + private url: string; + + private apiKey: string; + + private branch: string; + + private draft: boolean; + + private run: number; + + private headSha: string; + + private author: string; + + private run_url: string; + + private pr: number; + + constructor(options: { + url: string; + apiKey: string; + branch: string; + draft: boolean; + run: number; + headSha: string; + author: string; + run_url: string; + pr: number; + }) { + this.url = options.url; + this.apiKey = options.apiKey; + this.branch = options.branch; + this.draft = options.draft; + this.run = options.run; + this.headSha = options.headSha; + this.author = options.author; + this.run_url = options.run_url; + this.pr = options.pr; + } + + async onTestEnd(test: TestCase, result: TestResult) { + if (process.env.REPORTER_ROCKETCHAT_REPORT !== 'true') { + return; + } + + if (this.draft === true) { + return; + } + + if (result.status === 'passed' || result.status === 'skipped') { + return; + } + + const payload = { + name: test.title, + status: result.status, + duration: result.duration, + branch: this.branch, + draft: this.draft, + run: this.run, + headSha: this.headSha, + }; + + console.log(`Sending test result to JIRA: ${JSON.stringify(payload)}`); + + // first search and check if there is an existing issue + + const search = await fetch( + `${this.url}/rest/api/2/search?${new URLSearchParams({ + jql: `project = FLAKY AND summary ~ '${payload.name}'`, + })}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }, + ); + + if (!search.ok) { + throw new Error( + `JIRA: Failed to search for existing issue: ${search.statusText}.` + + `${this.url}/rest/api/2/search${new URLSearchParams({ + jql: `project = FLAKY AND summary ~ '${payload.name}'`, + })}`, + ); + } + + const { issues } = await search.json(); + + const existing = issues.find( + (issue: { + fields: { + summary: string; + }; + }) => issue.fields.summary === payload.name, + ); + + if (existing) { + const { location } = test; + + await fetch(`${this.url}/rest/api/2/issue/${existing.key}/comment`, { + method: 'POST', + body: JSON.stringify({ + body: `Test run ${payload.run} failed +author: ${this.author} +PR: ${this.pr} +https://github.com/RocketChat/Rocket.Chat/blob/${payload.headSha}/${location.file.replace( + '/home/runner/work/Rocket.Chat/Rocket.Chat', + '', + )}#L${location.line}:${location.column} +${this.run_url} +`, + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }); + return; + } + + const data: { + fields: { + summary: string; + description: string; + issuetype: { + name: string; + }; + project: { + key: string; + }; + }; + } = { + fields: { + summary: payload.name, + description: '', + issuetype: { + name: 'Tech Debt', + }, + project: { + key: 'FLAKY', + }, + }, + }; + + const responseIssue = await fetch(`${this.url}/rest/api/2/issue`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }); + + const issue = (await responseIssue.json()).key; + + const { location } = test; + + await fetch(`${this.url}/rest/api/2/issue/${issue}/comment`, { + method: 'POST', + body: JSON.stringify({ + body: `Test run ${payload.run} failed +author: ${this.author} +PR: ${this.pr} +https://github.com/RocketChat/Rocket.Chat/blob/${payload.headSha}/${location.file.replace( + '/home/runner/work/Rocket.Chat/Rocket.Chat', + '', + )}#L${location.line}:${location.column}, +${this.run_url} +`, + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }); + } +} + +export default JIRAReporter;