-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
QD-10723 Test summary, comment and push-fixes for azure pipelines
- Loading branch information
Andrei Iurko
committed
Jan 27, 2025
1 parent
79afc8e
commit 0bea3ca
Showing
13 changed files
with
67,424 additions
and
844 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright 2021-2024 JetBrains s.r.o. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export const FAILURE_LEVEL = 'failure' | ||
export const WARNING_LEVEL = 'warning' | ||
export const NOTICE_LEVEL = 'notice' | ||
|
||
export interface Annotation { | ||
title: string | undefined | ||
path: string | ||
start_line: number | ||
end_line: number | ||
level: 'failure' | 'warning' | 'notice' | ||
message: string | ||
start_column: number | undefined | ||
end_column: number | undefined | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
/* | ||
* Copyright 2021-2024 JetBrains s.r.o. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import {Coverage, QODANA_OPEN_IN_IDE_NAME, QODANA_REPORT_URL_NAME, VERSION} from "./qodana"; | ||
import * as fs from "fs"; | ||
import {Annotation, FAILURE_LEVEL, NOTICE_LEVEL, WARNING_LEVEL} from "./annotations"; | ||
|
||
export const COMMIT_USER = 'qodana-bot' | ||
export const COMMIT_EMAIL = 'qodana-support@jetbrains.com' | ||
export const QODANA_CHECK_NAME = 'Qodana' | ||
const UNKNOWN_RULE_ID = 'Unknown' | ||
const SUMMARY_TABLE_HEADER = '| Inspection name | Severity | Problems |' | ||
const SUMMARY_TABLE_SEP = '| --- | --- | --- |' | ||
const SUMMARY_MISC = `Contact us at [qodana-support@jetbrains.com](mailto:qodana-support@jetbrains.com) | ||
- Or via our issue tracker: https://jb.gg/qodana-issue | ||
- Or share your feedback: https://jb.gg/qodana-discussions` | ||
const VIEW_REPORT_OPTIONS = `To be able to view the detailed Qodana report, you can either: | ||
- Register at [Qodana Cloud](https://qodana.cloud/) and [configure the action](https://github.com/jetbrains/qodana-action#qodana-cloud) | ||
- Use [GitHub Code Scanning with Qodana](https://github.com/jetbrains/qodana-action#github-code-scanning) | ||
- Host [Qodana report at GitHub Pages](https://github.com/JetBrains/qodana-action/blob/3a8e25f5caad8d8b01c1435f1ef7b19fe8b039a0/README.md#github-pages) | ||
- Inspect and use \`qodana.sarif.json\` (see [the Qodana SARIF format](https://www.jetbrains.com/help/qodana/qodana-sarif-output.html#Report+structure) for details) | ||
To get \`*.log\` files or any other Qodana artifacts, run the action with \`upload-result\` option set to \`true\`, | ||
so that the action will upload the files as the job artifacts: | ||
\`\`\`yaml | ||
- name: 'Qodana Scan' | ||
uses: JetBrains/qodana-action@v${VERSION} | ||
with: | ||
upload-result: true | ||
\`\`\` | ||
` | ||
const SUMMARY_PR_MODE = `💡 Qodana analysis was run in the pull request mode: only the changed files were checked` | ||
|
||
interface CloudData { | ||
url?: string | ||
} | ||
|
||
interface OpenInIDEData { | ||
cloud?: CloudData | ||
} | ||
|
||
export interface LicenseEntry { | ||
name?: string | ||
version?: string | ||
license?: string | ||
} | ||
|
||
function wrapToDiffBlock(message: string): string { | ||
return `\`\`\`diff | ||
${message} | ||
\`\`\`` | ||
} | ||
|
||
export function getCoverageStats(c: Coverage): string { | ||
if (c.totalLines === 0 && c.totalCoveredLines === 0) { | ||
return '' | ||
} | ||
|
||
let stats = '' | ||
if (c.totalLines !== 0) { | ||
let conclusion = `${c.totalCoverage}% total lines covered` | ||
if (c.totalCoverage < c.totalCoverageThreshold) { | ||
conclusion = `- ${conclusion}` | ||
} else { | ||
conclusion = `+ ${conclusion}` | ||
} | ||
stats += `${conclusion} | ||
${c.totalLines} lines analyzed, ${c.totalCoveredLines} lines covered` | ||
} | ||
|
||
if (c.freshLines !== 0) { | ||
stats += ` | ||
! ${c.freshCoverage}% fresh lines covered | ||
${c.freshLines} lines analyzed, ${c.freshCoveredLines} lines covered` | ||
} | ||
|
||
return wrapToDiffBlock( | ||
[ | ||
`@@ Code coverage @@`, | ||
`${stats}`, | ||
`# Calculated according to the filters of your coverage tool` | ||
].join('\n') | ||
) | ||
} | ||
|
||
export function getReportURL(resultsDir: string): string { | ||
let reportUrlFile = `${resultsDir}/${QODANA_OPEN_IN_IDE_NAME}` | ||
if (fs.existsSync(reportUrlFile)) { | ||
const rawData = fs.readFileSync(reportUrlFile, {encoding: 'utf8'}) | ||
const data = JSON.parse(rawData) as OpenInIDEData | ||
if (data?.cloud?.url) { | ||
return data.cloud.url | ||
} | ||
} else { | ||
reportUrlFile = `${resultsDir}/${QODANA_REPORT_URL_NAME}` | ||
if (fs.existsSync(reportUrlFile)) { | ||
return fs.readFileSync(reportUrlFile, {encoding: 'utf8'}) | ||
} | ||
} | ||
return '' | ||
} | ||
|
||
function wrapToToggleBlock(header: string, body: string): string { | ||
return `<details> | ||
<summary>${header}</summary> | ||
${body} | ||
</details>` | ||
} | ||
|
||
function getViewReportText(reportUrl: string): string { | ||
if (reportUrl !== '') { | ||
return `☁️ [View the detailed Qodana report](${reportUrl})` | ||
} | ||
return wrapToToggleBlock( | ||
'View the detailed Qodana report', | ||
VIEW_REPORT_OPTIONS | ||
) | ||
} | ||
|
||
/** | ||
* Generates a table row for a given level. | ||
* @param annotations The annotations to generate the table row from. | ||
* @param level The level to generate the table row for. | ||
*/ | ||
function getRowsByLevel(annotations: Annotation[], level: string): string { | ||
const problems = annotations.reduce( | ||
(map: Map<string, number>, e) => | ||
map.set( | ||
e.title ?? UNKNOWN_RULE_ID, | ||
map.get(e.title ?? UNKNOWN_RULE_ID) !== undefined | ||
? map.get(e.title ?? UNKNOWN_RULE_ID)! + 1 | ||
: 1 | ||
), | ||
new Map() | ||
) | ||
return Array.from(problems.entries()) | ||
.sort((a, b) => b[1] - a[1]) | ||
.map(([title, count]) => `| \`${title}\` | ${level} | ${count} |`) | ||
.join('\n') | ||
} | ||
|
||
/** | ||
* Generates action summary string of annotations. | ||
* @param toolName The name of the tool to generate the summary from. | ||
* @param projectDir The path to the project. | ||
* @param annotations The annotations to generate the summary from. | ||
* @param coverageInfo The coverage is a Markdown text to generate the summary from. | ||
* @param packages The number of dependencies in the analyzed project. | ||
* @param licensesInfo The licenses a Markdown text to generate the summary from. | ||
* @param reportUrl The URL to the Qodana report. | ||
* @param prMode Whether the analysis was run in the pull request mode. | ||
* @param dependencyCharsLimit Limit on how many characters can be included in comment | ||
*/ | ||
export function getSummary( | ||
toolName: string, | ||
projectDir: string, | ||
annotations: Annotation[], | ||
coverageInfo: string, | ||
packages: number, | ||
licensesInfo: string, | ||
reportUrl: string, | ||
prMode: boolean, | ||
dependencyCharsLimit: number | ||
): string { | ||
const contactBlock = wrapToToggleBlock('Contact Qodana team', SUMMARY_MISC) | ||
let licensesBlock = '' | ||
if (licensesInfo !== '' && licensesInfo.length < dependencyCharsLimit) { | ||
licensesBlock = wrapToToggleBlock( | ||
`Detected ${packages} ${getDepencencyPlural(packages)}`, | ||
licensesInfo | ||
) | ||
} | ||
let prModeBlock = '' | ||
if (prMode) { | ||
prModeBlock = SUMMARY_PR_MODE | ||
} | ||
if (reportUrl !== '') { | ||
const firstToolName = toolName.split(' ')[0] | ||
toolName = toolName.replace( | ||
firstToolName, | ||
`[${firstToolName}](${reportUrl})` | ||
) | ||
} | ||
if (annotations.length === 0) { | ||
return [ | ||
`# ${toolName}`, | ||
projectDir === '' ? '' : ['`', projectDir, '/`\n'].join(''), | ||
'**It seems all right 👌**', | ||
'', | ||
'No new problems were found according to the checks applied', | ||
coverageInfo, | ||
prModeBlock, | ||
getViewReportText(reportUrl), | ||
licensesBlock, | ||
contactBlock | ||
].join('\n') | ||
} | ||
|
||
return [ | ||
`# ${toolName}`, | ||
projectDir === '' ? '' : ['`', projectDir, '/`\n'].join(''), | ||
`**${annotations.length} ${getProblemPlural( | ||
annotations.length | ||
)}** were found`, | ||
'', | ||
SUMMARY_TABLE_HEADER, | ||
SUMMARY_TABLE_SEP, | ||
[ | ||
getRowsByLevel( | ||
annotations.filter(a => a.level === FAILURE_LEVEL), | ||
'🔴 Failure' | ||
), | ||
getRowsByLevel( | ||
annotations.filter(a => a.level === WARNING_LEVEL), | ||
'🔶 Warning' | ||
), | ||
getRowsByLevel( | ||
annotations.filter(a => a.level === NOTICE_LEVEL), | ||
'◽️ Notice' | ||
) | ||
] | ||
.filter(e => e !== '') | ||
.join('\n'), | ||
'', | ||
coverageInfo, | ||
prModeBlock, | ||
getViewReportText(reportUrl), | ||
licensesBlock, | ||
contactBlock | ||
].join('\n') | ||
} | ||
|
||
/** | ||
* Generates a plural form of the word "problem" depending on the given count. | ||
* @param count A number representing the count of problems | ||
* @returns A formatted string with the correct plural form of "problem" | ||
*/ | ||
export function getProblemPlural(count: number): string { | ||
return `new problem${count !== 1 ? 's' : ''}` | ||
} | ||
|
||
/** | ||
* Generates a plural form of the word "dependency" depending on the given count. | ||
* @param count A number representing the count of dependencies | ||
* @returns A formatted string with the correct plural form of "dependency" | ||
*/ | ||
export function getDepencencyPlural(count: number): string { | ||
return `dependenc${count !== 1 ? 'ies' : 'y'}` | ||
} | ||
|
||
/* | ||
* The pull request with quick-fixes body template. | ||
*/ | ||
export function prFixesBody(jobUrl: string): string { | ||
return ` 🖐 Hey there! | ||
This pull request has been auto-generated by the [Qodana Scan workflow](${jobUrl}) configured in your repository. | ||
It has performed code analysis and applied some suggested fixes to improve your code quality 🧹✨ | ||
> **Warning** | ||
> It's crucial to review these changes to ensure everything shipshape manually. Please take a moment to examine the changes here. Remember to run your integration tests against this PR to validate the fixes and ensure everything's functioning as expected. | ||
_💻🔍 Happy reviewing and testing! | ||
Best, | ||
[Qodana Scan 🤖](https://github.com/marketplace/actions/qodana-scan)_` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/* | ||
* Copyright 2021-2024 JetBrains s.r.o. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import type {Tool} from 'sarif' | ||
|
||
export interface Rule { | ||
shortDescription: string | ||
fullDescription: string | ||
} | ||
|
||
/** | ||
* Extracts the rules descriptions from SARIF tool field. | ||
* @param tool the SARIF tool field. | ||
* @returns The map of SARIF rule IDs to their descriptions. | ||
*/ | ||
export function parseRules(tool: Tool): Map<string, Rule> { | ||
const rules = new Map<string, Rule>() | ||
tool.driver.rules?.forEach(rule => { | ||
rules.set(rule.id, { | ||
shortDescription: rule.shortDescription!.text, | ||
fullDescription: | ||
rule.fullDescription!.markdown || rule.fullDescription!.text | ||
}) | ||
}) | ||
|
||
tool?.extensions?.forEach(ext => { | ||
ext?.rules?.forEach(rule => { | ||
rules.set(rule.id, { | ||
shortDescription: rule.shortDescription!.text, | ||
fullDescription: | ||
rule.fullDescription!.markdown || rule.fullDescription!.text | ||
}) | ||
}) | ||
}) | ||
return rules | ||
} |
Oops, something went wrong.