mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
ci(danger-gitlab): migrate code; Danger Gitlab from Shared-CI-Danger
- set only brew runners to run danger, tag "dangerjs"
This commit is contained in:
parent
394a6ec351
commit
742ec21371
@ -18,6 +18,7 @@ workflow:
|
||||
# Place the default settings in `.gitlab/ci/common.yml` instead
|
||||
|
||||
include:
|
||||
- '.gitlab/ci/danger.yml'
|
||||
- '.gitlab/ci/common.yml'
|
||||
- '.gitlab/ci/rules.yml'
|
||||
- '.gitlab/ci/upload_cache.yml'
|
||||
|
@ -52,7 +52,6 @@
|
||||
/.github/workflows/ @esp-idf-codeowners/ci
|
||||
/.gitlab-ci.yml @esp-idf-codeowners/ci
|
||||
/.gitlab/ci/ @esp-idf-codeowners/ci
|
||||
/.gitlab/dangerjs/ @esp-idf-codeowners/ci @esp-idf-codeowners/tools
|
||||
/.pre-commit-config.yaml @esp-idf-codeowners/ci
|
||||
/.readthedocs.yml @esp-idf-codeowners/docs
|
||||
/.vale.ini @esp-idf-codeowners/docs
|
||||
|
18
.gitlab/ci/danger.yml
Normal file
18
.gitlab/ci/danger.yml
Normal file
@ -0,0 +1,18 @@
|
||||
# Extenal DangerJS
|
||||
include:
|
||||
- project: espressif/shared-ci-dangerjs
|
||||
ref: master
|
||||
file: danger.yaml
|
||||
|
||||
run-danger-mr-linter:
|
||||
stage: pre_check
|
||||
variables:
|
||||
GIT_STRATEGY: none # no repo checkout
|
||||
ENABLE_CHECK_AREA_LABELS: 'true'
|
||||
ENABLE_CHECK_DOCS_TRANSLATION: 'true'
|
||||
ENABLE_CHECK_RELEASE_NOTES_DESCRIPTION: 'true'
|
||||
ENABLE_CHECK_UPDATED_CHANGELOG: 'false'
|
||||
before_script: []
|
||||
cache: []
|
||||
tags:
|
||||
- dangerjs
|
@ -16,33 +16,6 @@ check_pre_commit:
|
||||
- fetch_submodules
|
||||
- pre-commit run --files $MODIFIED_FILES
|
||||
|
||||
check_MR_style_dangerjs:
|
||||
extends:
|
||||
- .pre_check_template
|
||||
image: node:18.15.0-alpine3.16
|
||||
variables:
|
||||
DANGER_GITLAB_API_TOKEN: ${ESPCI_TOKEN}
|
||||
DANGER_GITLAB_HOST: ${GITLAB_HTTP_SERVER}
|
||||
DANGER_GITLAB_API_BASE_URL: ${GITLAB_HTTP_SERVER}/api/v4
|
||||
DANGER_JIRA_USER: ${DANGER_JIRA_USER}
|
||||
DANGER_JIRA_PASSWORD: ${DANGER_JIRA_PASSWORD}
|
||||
cache:
|
||||
# pull only for most of the use cases since it's cache dir.
|
||||
# Only set "push" policy for "upload_cache" stage jobs
|
||||
key:
|
||||
files:
|
||||
- .gitlab/dangerjs/package-lock.json
|
||||
paths:
|
||||
- .gitlab/dangerjs/node_modules/
|
||||
policy: pull
|
||||
before_script:
|
||||
- cd .gitlab/dangerjs
|
||||
- npm install --no-progress --no-update-notifier # Install danger dependencies
|
||||
script:
|
||||
- npx danger ci --failOnErrors -v
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
|
||||
check_version:
|
||||
# Don't run this for feature/bugfix branches, so that it is possible to modify
|
||||
# esp_idf_version.h in a branch before tagging the next version.
|
||||
|
@ -48,27 +48,3 @@ upload-submodules-cache:
|
||||
parallel:
|
||||
matrix:
|
||||
- GEO: [ 'shiny', 'brew' ]
|
||||
|
||||
upload-danger-npm-cache:
|
||||
stage: upload_cache
|
||||
image: node:18.15.0-alpine3.16
|
||||
extends:
|
||||
- .rules:patterns:dangerjs
|
||||
tags:
|
||||
- $GEO
|
||||
- cache
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- .gitlab/dangerjs/package-lock.json
|
||||
paths:
|
||||
- .gitlab/dangerjs/node_modules/
|
||||
policy: push
|
||||
before_script:
|
||||
- echo "Skip before scripts ...."
|
||||
script:
|
||||
- cd .gitlab/dangerjs
|
||||
- npm install --no-progress --no-update-notifier
|
||||
parallel:
|
||||
matrix:
|
||||
- GEO: [ 'shiny', 'brew' ]
|
||||
|
@ -1,172 +0,0 @@
|
||||
const {
|
||||
minimumSummaryChars,
|
||||
maximumSummaryChars,
|
||||
maximumBodyLineChars,
|
||||
allowedTypes,
|
||||
} = require("./mrCommitsConstants.js");
|
||||
const { gptStandardModelTokens } = require("./mrCommitsConstants.js");
|
||||
|
||||
const { ChatPromptTemplate } = require("langchain/prompts");
|
||||
const { SystemMessagePromptTemplate } = require("langchain/prompts");
|
||||
const { LLMChain } = require("langchain/chains");
|
||||
const { ChatOpenAI } = require("langchain/chat_models/openai");
|
||||
const openAiTokenCount = require("openai-gpt-token-counter");
|
||||
|
||||
module.exports = async function () {
|
||||
let outputDangerMessage = `\n\nPerhaps you could use an AI-generated suggestion for your commit message. Here is one `;
|
||||
|
||||
let mrDiff = await getMrGitDiff(danger.git.modified_files);
|
||||
const mrCommitMessages = getCommitMessages(danger.gitlab.commits);
|
||||
const inputPrompt = getInputPrompt();
|
||||
const inputLlmTokens = getInputLlmTokens(
|
||||
inputPrompt,
|
||||
mrDiff,
|
||||
mrCommitMessages
|
||||
);
|
||||
console.log(`Input tokens for LLM: ${inputLlmTokens}`);
|
||||
|
||||
if (inputLlmTokens >= gptStandardModelTokens) {
|
||||
mrDiff = ""; // If the input mrDiff is larger than 16k model, don't use mrDiff, use only current commit messages
|
||||
outputDangerMessage += `(based only on your current commit messages, git-diff of this MR is too big (${inputLlmTokens} tokens) for the AI models):\n\n`;
|
||||
} else {
|
||||
outputDangerMessage += `(based on your MR git-diff and your current commit messages):\n\n`;
|
||||
}
|
||||
|
||||
// Generate AI commit message
|
||||
let generatedCommitMessage = "";
|
||||
try {
|
||||
const rawCommitMessage = await createAiGitMessage(
|
||||
inputPrompt,
|
||||
mrDiff,
|
||||
mrCommitMessages
|
||||
);
|
||||
generatedCommitMessage = postProcessCommitMessage(rawCommitMessage);
|
||||
} catch (error) {
|
||||
console.error("Error in generating AI commit message: ", error);
|
||||
outputDangerMessage +=
|
||||
"\nCould not generate commit message due to an error.\n";
|
||||
}
|
||||
|
||||
// Append closing statements ("Closes https://github.com/espressif/esp-idf/issues/XXX") to the generated commit message
|
||||
let closingStatements = extractClosingStatements(mrCommitMessages);
|
||||
if (closingStatements.length > 0) {
|
||||
generatedCommitMessage += "\n\n" + closingStatements;
|
||||
}
|
||||
|
||||
// Add the generated git message, format to the markdown code block
|
||||
outputDangerMessage += `\n\`\`\`\n${generatedCommitMessage}\n\`\`\`\n`;
|
||||
outputDangerMessage +=
|
||||
"\n**NOTE: AI-generated suggestions may not always be correct, please review the suggestion before using it.**"; // Add disclaimer
|
||||
return outputDangerMessage;
|
||||
};
|
||||
|
||||
async function getMrGitDiff(mrModifiedFiles) {
|
||||
const fileDiffs = await Promise.all(
|
||||
mrModifiedFiles.map((file) => danger.git.diffForFile(file))
|
||||
);
|
||||
return fileDiffs.map((fileDiff) => fileDiff.diff.trim()).join(" ");
|
||||
}
|
||||
|
||||
function getCommitMessages(mrCommits) {
|
||||
return mrCommits.map((commit) => commit.message);
|
||||
}
|
||||
|
||||
function getInputPrompt() {
|
||||
return `You are a helpful assistant that creates suggestions for single git commit message, that user can use to describe all the changes in their merge request.
|
||||
Use git diff: {mrDiff} and users current commit messages: {mrCommitMessages} to get the changes made in the commit.
|
||||
|
||||
Output should be git commit message following the conventional commit format.
|
||||
|
||||
Output only git commit message in desired format, without comments and other text.
|
||||
|
||||
Do not include the closing statements ("Closes https://....") in the output.
|
||||
|
||||
Here are the strict rules you must follow:
|
||||
|
||||
- Avoid mentioning any JIRA tickets (e.g., "Closes JIRA-123").
|
||||
- Be specific. Don't use vague terms (e.g., "some checks", "add new ones", "few changes").
|
||||
- The commit message structure should be: <type><(scope/component)>: <summary>
|
||||
- Types allowed: ${allowedTypes.join(", ")}
|
||||
- If 'scope/component' is used, it must start with a lowercase letter.
|
||||
- The 'summary' must NOT end with a period.
|
||||
- The 'summary' must be between ${minimumSummaryChars} and ${maximumSummaryChars} characters long.
|
||||
|
||||
If a 'body' of commit message is used:
|
||||
|
||||
- Each line must be no longer than ${maximumBodyLineChars} characters.
|
||||
- It must be separated from the 'summary' by a blank line.
|
||||
|
||||
Examples of correct commit messages:
|
||||
|
||||
- With scope and body:
|
||||
fix(freertos): Fix startup timeout issue
|
||||
|
||||
This is a text of commit message body...
|
||||
- adds support for wifi6
|
||||
- adds validations for logging script
|
||||
|
||||
- Without scope and body:
|
||||
ci: added target test job for ESP32-Wifi6`;
|
||||
}
|
||||
|
||||
function getInputLlmTokens(inputPrompt, mrDiff, mrCommitMessages) {
|
||||
const mrCommitMessagesTokens = openAiTokenCount(mrCommitMessages.join(" "));
|
||||
const gitDiffTokens = openAiTokenCount(mrDiff);
|
||||
const promptTokens = openAiTokenCount(inputPrompt);
|
||||
return mrCommitMessagesTokens + gitDiffTokens + promptTokens;
|
||||
}
|
||||
|
||||
async function createAiGitMessage(inputPrompt, mrDiff, mrCommitMessages) {
|
||||
const chat = new ChatOpenAI({ engine: "gpt-3.5-turbo", temperature: 0 });
|
||||
const chatPrompt = ChatPromptTemplate.fromPromptMessages([
|
||||
SystemMessagePromptTemplate.fromTemplate(inputPrompt),
|
||||
]);
|
||||
const chain = new LLMChain({ prompt: chatPrompt, llm: chat });
|
||||
|
||||
const response = await chain.call({
|
||||
mrDiff: mrDiff,
|
||||
mrCommitMessages: mrCommitMessages,
|
||||
});
|
||||
return response.text;
|
||||
}
|
||||
|
||||
function postProcessCommitMessage(rawCommitMessage) {
|
||||
// Split the result into lines
|
||||
let lines = rawCommitMessage.split("\n");
|
||||
|
||||
// Format each line
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i].trim();
|
||||
|
||||
// If the line is longer than maximumBodyLineChars, split it into multiple lines
|
||||
if (line.length > maximumBodyLineChars) {
|
||||
let newLines = [];
|
||||
while (line.length > maximumBodyLineChars) {
|
||||
let lastSpaceIndex = line.lastIndexOf(
|
||||
" ",
|
||||
maximumBodyLineChars
|
||||
);
|
||||
newLines.push(line.substring(0, lastSpaceIndex));
|
||||
line = line.substring(lastSpaceIndex + 1);
|
||||
}
|
||||
newLines.push(line);
|
||||
lines[i] = newLines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Join the lines back into a single string with a newline between each one
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function extractClosingStatements(mrCommitMessages) {
|
||||
let closingStatements = [];
|
||||
mrCommitMessages.forEach((message) => {
|
||||
const lines = message.split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.startsWith("Closes")) {
|
||||
closingStatements.push(line);
|
||||
}
|
||||
});
|
||||
});
|
||||
return closingStatements.join("\n");
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
let outputStatuses = [];
|
||||
|
||||
/**
|
||||
* Logs the status of a rule with padded formatting and stores it in the `outputStatuses` array.
|
||||
* If the rule already exists in the array, its status is updated.
|
||||
* @param message The name of the rule
|
||||
* @param status The output (exit) status of the rule
|
||||
*/
|
||||
function recordRuleExitStatus(message, status) {
|
||||
// Check if the rule already exists in the array
|
||||
const existingRecord = outputStatuses.find(
|
||||
(rule) => rule.message === message
|
||||
);
|
||||
|
||||
if (existingRecord) {
|
||||
// Update the status of the existing rule
|
||||
existingRecord.status = status;
|
||||
} else {
|
||||
// If the rule doesn't exist, add it to the array
|
||||
outputStatuses.push({ message, status });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays all the rule output statuses stored in the `outputStatuses` array.
|
||||
* Filters out any empty lines, sorts them alphabetically, and prints the statuses
|
||||
* with a header and separator.
|
||||
* These statuses are later displayed in CI job tracelog.
|
||||
*/
|
||||
function displayAllOutputStatuses() {
|
||||
const lineLength = 100;
|
||||
const sortedStatuses = outputStatuses.sort((a, b) =>
|
||||
a.message.localeCompare(b.message)
|
||||
);
|
||||
|
||||
const formattedLines = sortedStatuses.map((statusObj) => {
|
||||
const paddingLength =
|
||||
lineLength - statusObj.message.length - statusObj.status.length;
|
||||
const paddedMessage = statusObj.message.padEnd(
|
||||
statusObj.message.length + paddingLength,
|
||||
"."
|
||||
);
|
||||
return `${paddedMessage} ${statusObj.status}`;
|
||||
});
|
||||
|
||||
console.log(
|
||||
"DangerJS checks (rules) output states:\n" + "=".repeat(lineLength + 2)
|
||||
);
|
||||
console.log(formattedLines.join("\n"));
|
||||
console.log("=".repeat(lineLength + 2));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
displayAllOutputStatuses,
|
||||
recordRuleExitStatus,
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
const { displayAllOutputStatuses } = require("./configParameters.js");
|
||||
|
||||
/*
|
||||
* Modules with checks are stored in ".gitlab/dangerjs/<module_name>". To import them, use path relative to "dangerfile.js"
|
||||
*/
|
||||
async function runChecks() {
|
||||
// Checks for merge request title
|
||||
require("./mrTitleNoDraftOrWip.js")();
|
||||
|
||||
// Checks for merge request description
|
||||
require("./mrDescriptionLongEnough.js")();
|
||||
require("./mrDescriptionReleaseNotes.js")();
|
||||
await require("./mrDescriptionJiraLinks.js")();
|
||||
|
||||
// Checks for documentation
|
||||
await require("./mrDocsTranslation.js")();
|
||||
|
||||
// Checks for MR commits
|
||||
require("./mrCommitsTooManyCommits.js")();
|
||||
await require("./mrCommitsCommitMessage.js")();
|
||||
require("./mrCommitsEmail.js")();
|
||||
|
||||
// Checks for MR code
|
||||
require("./mrSizeTooLarge.js")();
|
||||
|
||||
// Checks for MR area labels
|
||||
await require("./mrAreaLabels.js")();
|
||||
|
||||
// Checks for Source branch name
|
||||
require("./mrSourceBranchName.js")();
|
||||
|
||||
// Show DangerJS individual checks statuses - visible in CI job tracelog
|
||||
displayAllOutputStatuses();
|
||||
|
||||
// Add success log if no issues
|
||||
if (
|
||||
results.fails.length === 0 &&
|
||||
results.warnings.length === 0 &&
|
||||
results.messages.length === 0
|
||||
) {
|
||||
return message("🎉 Good Job! All checks are passing!");
|
||||
}
|
||||
}
|
||||
|
||||
runChecks();
|
||||
|
||||
// Add retry link
|
||||
const retryLink = `${process.env.DANGER_GITLAB_HOST}/${process.env.CI_PROJECT_PATH}/-/jobs/${process.env.CI_JOB_ID}`;
|
||||
markdown(
|
||||
`***\n#### :repeat: You can enforce automatic MR checks by retrying the [DangerJS job](${retryLink})\n***`
|
||||
);
|
@ -1,27 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if MR has area labels (light blue labels)
|
||||
*
|
||||
* @dangerjs WARN
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const ruleName = "Merge request area labels";
|
||||
const projectId = 103; // ESP-IDF
|
||||
const areaLabelColor = /^#d2ebfa$/i; // match color code (case-insensitive)
|
||||
const projectLabels = await danger.gitlab.api.Labels.all(projectId); // Get all project labels
|
||||
const areaLabels = projectLabels
|
||||
.filter((label) => areaLabelColor.test(label.color))
|
||||
.map((label) => label.name); // Filter only area labels
|
||||
const mrLabels = danger.gitlab.mr.labels; // Get MR labels
|
||||
|
||||
if (!mrLabels.some((label) => areaLabels.includes(label))) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(
|
||||
`Please add some [area labels](${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/labels) to this MR.`
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, "Passed");
|
||||
};
|
@ -1,165 +0,0 @@
|
||||
const {
|
||||
minimumSummaryChars,
|
||||
maximumSummaryChars,
|
||||
maximumBodyLineChars,
|
||||
allowedTypes,
|
||||
} = require("./mrCommitsConstants.js");
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check that commit messages are based on the Espressif ESP-IDF project's rules for git commit messages.
|
||||
*
|
||||
* @dangerjs WARN
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const ruleName = "Commit messages style";
|
||||
const mrCommits = danger.gitlab.commits;
|
||||
const lint = require("@commitlint/lint").default;
|
||||
|
||||
const lintingRules = {
|
||||
// rule definition: [(0-1 = off/on), (always/never = must be/mustn't be), (value)]
|
||||
"body-max-line-length": [1, "always", maximumBodyLineChars], // Max length of the body line
|
||||
"footer-leading-blank": [1, "always"], // Always have a blank line before the footer section
|
||||
"footer-max-line-length": [1, "always", maximumBodyLineChars], // Max length of the footer line
|
||||
"subject-max-length": [1, "always", maximumSummaryChars], // Max length of the "Summary"
|
||||
"subject-min-length": [1, "always", minimumSummaryChars], // Min length of the "Summary"
|
||||
"scope-case": [1, "always", "lower-case"], // "scope/component" must start with lower-case
|
||||
"subject-full-stop": [1, "never", "."], // "Summary" must not end with a full stop (period)
|
||||
"subject-empty": [1, "never"], // "Summary" is mandatory
|
||||
"type-case": [1, "always", "lower-case"], // "type/action" must start with lower-case
|
||||
"type-empty": [1, "never"], // "type/action" is mandatory
|
||||
"type-enum": [1, "always", allowedTypes], // "type/action" must be one of the allowed types
|
||||
"body-leading-blank": [1, "always"], // Always have a blank line before the body section
|
||||
};
|
||||
|
||||
// Switcher for AI suggestions (for poor messages)
|
||||
let generateAISuggestion = false;
|
||||
|
||||
// Search for the messages in each commit
|
||||
let issuesAllCommitMessages = [];
|
||||
|
||||
for (const commit of mrCommits) {
|
||||
const commitMessage = commit.message;
|
||||
const commitMessageTitle = commit.title;
|
||||
|
||||
let issuesSingleCommitMessage = [];
|
||||
let reportSingleCommitMessage = "";
|
||||
|
||||
// Check if the commit message contains any Jira ticket references
|
||||
const jiraTicketRegex = /[A-Z0-9]+-[0-9]+/g;
|
||||
const jiraTicketMatches = commitMessage.match(jiraTicketRegex);
|
||||
if (jiraTicketMatches) {
|
||||
const jiraTicketNames = jiraTicketMatches.join(", ");
|
||||
issuesSingleCommitMessage.push(
|
||||
`- probably contains Jira ticket reference (\`${jiraTicketNames}\`). Please remove Jira tickets from commit messages.`
|
||||
);
|
||||
}
|
||||
|
||||
// Lint commit messages with @commitlint (Conventional Commits style)
|
||||
const result = await lint(commit.message, lintingRules);
|
||||
|
||||
for (const warning of result.warnings) {
|
||||
// Custom messages for each rule with terminology used by Espressif conventional commits guide
|
||||
switch (warning.name) {
|
||||
case "subject-max-length":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *summary* appears to be too long`
|
||||
);
|
||||
break;
|
||||
case "type-empty":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *type/action* looks empty`
|
||||
);
|
||||
break;
|
||||
case "type-case":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *type/action* should start with a lowercase letter`
|
||||
);
|
||||
|
||||
break;
|
||||
case "scope-empty":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *scope/component* looks empty`
|
||||
);
|
||||
break;
|
||||
case "scope-case":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *scope/component* should be lowercase without whitespace, allowed special characters are \`_\` \`/\` \`.\` \`,\` \`*\` \`-\` \`.\``
|
||||
);
|
||||
break;
|
||||
case "subject-empty":
|
||||
issuesSingleCommitMessage.push(`- *summary* looks empty`);
|
||||
generateAISuggestion = true;
|
||||
break;
|
||||
case "subject-min-length":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *summary* looks too short`
|
||||
);
|
||||
generateAISuggestion = true;
|
||||
break;
|
||||
case "subject-case":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *summary* should start with a capital letter`
|
||||
);
|
||||
break;
|
||||
case "subject-full-stop":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *summary* should not end with a period (full stop)`
|
||||
);
|
||||
break;
|
||||
case "type-enum":
|
||||
issuesSingleCommitMessage.push(
|
||||
`- *type/action* should be one of [${allowedTypes
|
||||
.map((type) => `\`${type}\``)
|
||||
.join(", ")}]`
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
issuesSingleCommitMessage.push(`- ${warning.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (issuesSingleCommitMessage.length) {
|
||||
reportSingleCommitMessage = `- the commit message \`"${commitMessageTitle}"\`:\n${issuesSingleCommitMessage
|
||||
.map((message) => ` ${message}`) // Indent each issue by 2 spaces
|
||||
.join("\n")}`;
|
||||
issuesAllCommitMessages.push(reportSingleCommitMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Create report
|
||||
if (issuesAllCommitMessages.length) {
|
||||
issuesAllCommitMessages.sort();
|
||||
const basicTips = [
|
||||
`- correct format of commit message should be: \`<type/action>(<scope/component>): <summary>\`, for example \`fix(esp32): Fixed startup timeout issue\``,
|
||||
`- allowed types are: \`${allowedTypes}\``,
|
||||
`- sufficiently descriptive message summary should be between ${minimumSummaryChars} to ${maximumSummaryChars} characters and start with upper case letter`,
|
||||
`- avoid Jira references in commit messages (unavailable/irrelevant for our customers)`,
|
||||
`- follow this [commit messages guide](${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/wikis/dev-proc/Commit-messages)`,
|
||||
];
|
||||
let dangerMessage = `\n**Some issues found for the commit messages in this MR:**\n${issuesAllCommitMessages.join(
|
||||
"\n"
|
||||
)}
|
||||
\n***
|
||||
\n**Please consider updating these commit messages** - here are some basic tips:\n${basicTips.join(
|
||||
"\n"
|
||||
)}
|
||||
\n \`TIP:\` You can install commit-msg pre-commit hook (\`pre-commit install -t pre-commit -t commit-msg\`) to run this check when committing.
|
||||
\n***
|
||||
`;
|
||||
|
||||
if (generateAISuggestion) {
|
||||
// Create AI generated suggestion for git commit message based of gitDiff and current commit messages
|
||||
const AImessageSuggestion =
|
||||
await require("./aiGenerateGitMessage.js")();
|
||||
dangerMessage += AImessageSuggestion;
|
||||
}
|
||||
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(dangerMessage);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, "Passed");
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
module.exports = {
|
||||
gptStandardModelTokens: 4096,
|
||||
minimumSummaryChars: 20,
|
||||
maximumSummaryChars: 72,
|
||||
maximumBodyLineChars: 100,
|
||||
allowedTypes: [
|
||||
"change",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"refactor",
|
||||
"remove",
|
||||
"revert",
|
||||
],
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if the author is accidentally making a commit using a personal email
|
||||
*
|
||||
* @dangerjs INFO
|
||||
*/
|
||||
module.exports = function () {
|
||||
const ruleName = 'Commits from outside Espressif';
|
||||
const mrCommitAuthorEmails = danger.gitlab.commits.map(commit => commit.author_email);
|
||||
const mrCommitCommitterEmails = danger.gitlab.commits.map(commit => commit.committer_email);
|
||||
const emailPattern = /.*@espressif\.com/;
|
||||
const filteredEmails = [...mrCommitAuthorEmails, ...mrCommitCommitterEmails].filter((email) => !emailPattern.test(email));
|
||||
if (filteredEmails.length) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return message(
|
||||
`Some of the commits were authored or committed by developers outside Espressif: ${filteredEmails.join(', ')}. Please check if this is expected.`
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, 'Passed');
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if MR has not an excessive numbers of commits (if squashed)
|
||||
*
|
||||
* @dangerjs INFO
|
||||
*/
|
||||
module.exports = function () {
|
||||
const ruleName = 'Number of commits in merge request';
|
||||
const tooManyCommitThreshold = 2; // above this number of commits, squash commits is suggested
|
||||
const mrCommits = danger.gitlab.commits;
|
||||
|
||||
if (mrCommits.length > tooManyCommitThreshold) {
|
||||
recordRuleExitStatus(ruleName, "Passed (with suggestions)");
|
||||
return message(
|
||||
`You might consider squashing your ${mrCommits.length} commits (simplifying branch history).`
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, 'Passed');
|
||||
};
|
@ -1,238 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/** Check that there are valid JIRA links in MR description.
|
||||
*
|
||||
* This check extracts the "Related" section from the MR description and
|
||||
* searches for JIRA ticket references in the format "Closes [JIRA ticket key]".
|
||||
*
|
||||
* It then extracts the closing GitHub links from the corresponding JIRA tickets and
|
||||
* checks if the linked GitHub issues are still in open state.
|
||||
*
|
||||
* Finally, it checks if the required GitHub closing links are present in the MR's commit messages.
|
||||
*
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const ruleName = 'Jira ticket references';
|
||||
const axios = require("axios");
|
||||
const mrDescription = danger.gitlab.mr.description;
|
||||
const mrCommitMessages = danger.gitlab.commits.map(
|
||||
(commit) => commit.message
|
||||
);
|
||||
const jiraTicketRegex = /[A-Z0-9]+-[0-9]+/;
|
||||
|
||||
let partMessages = []; // Create a blank field for future records of individual issues
|
||||
|
||||
// Parse section "Related" from MR Description
|
||||
const sectionRelated = extractSectionRelated(mrDescription);
|
||||
|
||||
if (
|
||||
!sectionRelated.header || // No section Related in MR description or ...
|
||||
!jiraTicketRegex.test(sectionRelated.content) // no Jira links in section Related
|
||||
) {
|
||||
recordRuleExitStatus(ruleName, 'Passed (with suggestions)');
|
||||
return message(
|
||||
"Please consider adding references to JIRA issues in the `Related` section of the MR description."
|
||||
);
|
||||
}
|
||||
|
||||
// Get closing (only) JIRA tickets
|
||||
const jiraTickets = findClosingJiraTickets(sectionRelated.content);
|
||||
|
||||
for (const ticket of jiraTickets) {
|
||||
ticket.jiraUIUrl = `https://jira.espressif.com:8443/browse/${ticket.ticketName}`;
|
||||
|
||||
if (!ticket.correctFormat) {
|
||||
partMessages.push(
|
||||
`- closing ticket \`${ticket.record}\` seems to be in the wrong format (or inaccessible to Jira DangerBot).. The correct format is for example \`- Closes JIRA-123\`.`
|
||||
);
|
||||
}
|
||||
|
||||
// Get closing GitHub issue links from JIRA tickets
|
||||
const closingGithubLink = await getGitHubClosingLink(ticket.ticketName);
|
||||
if (closingGithubLink) {
|
||||
ticket.closingGithubLink = closingGithubLink;
|
||||
} else if (closingGithubLink === null) {
|
||||
partMessages.push(
|
||||
`- the Jira issue number [\`${ticket.ticketName}\`](${ticket.jiraUIUrl}) seems to be invalid (please check if the ticket number is correct)`
|
||||
);
|
||||
continue; // Handle unreachable JIRA tickets; skip the following checks
|
||||
} else {
|
||||
continue; // Jira ticket have no GitHub closing link; skip the following checks
|
||||
}
|
||||
|
||||
// Get still open GitHub issues
|
||||
const githubIssueStatusOpen = await isGithubIssueOpen(
|
||||
ticket.closingGithubLink
|
||||
);
|
||||
ticket.isOpen = githubIssueStatusOpen;
|
||||
if (githubIssueStatusOpen === null) {
|
||||
// Handle unreachable GitHub issues
|
||||
partMessages.push(
|
||||
`- the GitHub issue [\`${ticket.closingGithubLink}\`](${ticket.closingGithubLink}) does not seem to exist on GitHub (referenced from JIRA ticket [\`${ticket.ticketName}\`](${ticket.jiraUIUrl}) )`
|
||||
);
|
||||
continue; // skip the following checks
|
||||
}
|
||||
|
||||
// Search in commit message if there are all GitHub closing links (from Related section) for still open GH issues
|
||||
if (ticket.isOpen) {
|
||||
if (
|
||||
!mrCommitMessages.some((item) =>
|
||||
item.includes(`Closes ${ticket.closingGithubLink}`)
|
||||
)
|
||||
) {
|
||||
partMessages.push(
|
||||
`- please add \`Closes ${ticket.closingGithubLink}\` to the commit message`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create report / DangerJS check feedback if issues with Jira links found
|
||||
if (partMessages.length) {
|
||||
createReport();
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, 'Passed');
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This function takes in a string mrDescription which contains a Markdown-formatted text
|
||||
* related to a Merge Request (MR) in a GitLab repository. It searches for a section titled "Related"
|
||||
* and extracts the content of that section. If the section is not found, it returns an object
|
||||
* indicating that the header and content are null. If the section is found but empty, it returns
|
||||
* an object indicating that the header is present but the content is null. If the section is found
|
||||
* with content, it returns an object indicating that the header is present and the content of the
|
||||
* "Related" section.
|
||||
*
|
||||
* @param {string} mrDescription - The Markdown-formatted text related to the Merge Request.
|
||||
* @returns {{
|
||||
* header: string | boolean | null,
|
||||
* content: string | null
|
||||
* }} - An object containing the header and content of the "Related" section, if present.
|
||||
*/
|
||||
|
||||
function extractSectionRelated(mrDescription) {
|
||||
const regexSectionRelated = /## Related([\s\S]*?)(?=## |$)/;
|
||||
const sectionRelated = mrDescription.match(regexSectionRelated);
|
||||
if (!sectionRelated) {
|
||||
return { header: null, content: null }; // Section "Related" is missing
|
||||
}
|
||||
|
||||
const content = sectionRelated[1].replace(/(\r\n|\n|\r)/gm, ""); // Remove empty lines
|
||||
if (!content.length) {
|
||||
return { header: true, content: null }; // Section "Related" is present, but empty
|
||||
}
|
||||
|
||||
return { header: true, content: sectionRelated[1] }; // Found section "Related" with content
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all JIRA tickets that are being closed in the given sectionRelatedcontent.
|
||||
* The function searches for lines that start with - Closes and have the format Closes [uppercase letters]-[numbers].
|
||||
* @param {string} sectionRelatedcontent - A string that contains lines with mentions of JIRA tickets
|
||||
* @returns {Array} An array of objects with ticketName property that has the correct format
|
||||
*/
|
||||
|
||||
function findClosingJiraTickets(sectionRelatedcontent) {
|
||||
let closingTickets = [];
|
||||
const lines = sectionRelatedcontent.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("- Closes")) {
|
||||
continue; // Not closing-type ticket, skip
|
||||
}
|
||||
|
||||
const correctJiraClosingLinkFormat = new RegExp(
|
||||
`^- Closes ${jiraTicketRegex.source}$`
|
||||
);
|
||||
const matchedJiraTicket = line.match(jiraTicketRegex);
|
||||
if (matchedJiraTicket) {
|
||||
if (!correctJiraClosingLinkFormat.test(line)) {
|
||||
closingTickets.push({
|
||||
record: line,
|
||||
ticketName: matchedJiraTicket[0],
|
||||
correctFormat: false,
|
||||
});
|
||||
} else {
|
||||
closingTickets.push({
|
||||
record: line,
|
||||
ticketName: matchedJiraTicket[0],
|
||||
correctFormat: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return closingTickets;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes a JIRA issue key and retrieves the description from JIRA's API.
|
||||
* It then searches the description for a GitHub closing link in the format "Closes https://github.com/owner/repo/issues/123".
|
||||
* If a GitHub closing link is found, it is returned. If no GitHub closing link is found, it returns null.
|
||||
* @param {string} jiraIssueKey - The key of the JIRA issue to search for the GitHub closing link.
|
||||
* @returns {Promise<string|null>} - A promise that resolves to a string containing the GitHub closing link if found,
|
||||
* or null if not found.
|
||||
*/
|
||||
async function getGitHubClosingLink(jiraIssueKey) {
|
||||
let jiraDescription = "";
|
||||
|
||||
// Get JIRA ticket description content
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `https://jira.espressif.com:8443/rest/api/latest/issue/${jiraIssueKey}`,
|
||||
auth: {
|
||||
username: process.env.DANGER_JIRA_USER,
|
||||
password: process.env.DANGER_JIRA_PASSWORD,
|
||||
},
|
||||
});
|
||||
jiraDescription = response.data.fields.description
|
||||
? response.data.fields.description
|
||||
: ""; // if the Jira ticket has an unfilled Description, the ".description" property is missing in API response - in that case set "jiraDescription" to an empty string
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find GitHub closing link in description
|
||||
const regexClosingGhLink =
|
||||
/Closes\s+(https:\/\/github.com\/\S+\/\S+\/issues\/\d+)/;
|
||||
const closingGithubLink = jiraDescription.match(regexClosingGhLink);
|
||||
|
||||
if (closingGithubLink) {
|
||||
return closingGithubLink[1];
|
||||
} else {
|
||||
return false; // Jira issue has no GitHub closing link in description
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a GitHub issue linked in a merge request is still open.
|
||||
*
|
||||
* @param {string} link - The link to the GitHub issue.
|
||||
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the issue is open.
|
||||
* @throws {Error} If the link is invalid or if there was an error fetching the issue.
|
||||
*/
|
||||
async function isGithubIssueOpen(link) {
|
||||
const parsedUrl = new URL(link);
|
||||
const [owner, repo] = parsedUrl.pathname.split("/").slice(1, 3);
|
||||
const issueNumber = parsedUrl.pathname.split("/").slice(-1)[0];
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`
|
||||
);
|
||||
return response.data.state === "open"; // return True if GitHub issue is open
|
||||
} catch (error) {
|
||||
return null; // GET request to issue fails
|
||||
}
|
||||
}
|
||||
|
||||
function createReport() {
|
||||
partMessages.sort();
|
||||
let dangerMessage = `Some issues found for the related JIRA tickets in this MR:\n${partMessages.join(
|
||||
"\n"
|
||||
)}`;
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(dangerMessage);
|
||||
}
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if MR Description has accurate description".
|
||||
*
|
||||
* @dangerjs WARN
|
||||
*/
|
||||
module.exports = function () {
|
||||
const ruleName = "Merge request sufficient description";
|
||||
const mrDescription = danger.gitlab.mr.description;
|
||||
const descriptionChunk = mrDescription.match(/^([^#]*)/)[1].trim(); // Extract all text before the first section header (i.e., the text before the "## Release notes")
|
||||
|
||||
const shortMrDescriptionThreshold = 50; // Description is considered too short below this number of characters
|
||||
|
||||
if (descriptionChunk.length < shortMrDescriptionThreshold) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(
|
||||
"The MR description looks very brief, please check if more details can be added."
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, "Passed");
|
||||
};
|
@ -1,103 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if MR Description contains mandatory section "Release notes"
|
||||
*
|
||||
* Extracts the content of the "Release notes" section from the GitLab merge request description.
|
||||
*
|
||||
* @dangerjs WARN (if section missing, is empty or wrong markdown format)
|
||||
*/
|
||||
module.exports = function () {
|
||||
const ruleName = 'Merge request Release Notes section';
|
||||
const mrDescription = danger.gitlab.mr.description;
|
||||
const wiki_link = `${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/wikis/rfc/How-to-write-release-notes-properly`;
|
||||
|
||||
const regexSectionReleaseNotes = /## Release notes([\s\S]*?)(?=## |$)/;
|
||||
const regexValidEntry = /^\s*[-*+]\s+.+/;
|
||||
const regexNoReleaseNotes = /no release note/i;
|
||||
|
||||
const sectionReleaseNotes = mrDescription.match(regexSectionReleaseNotes);
|
||||
if (!sectionReleaseNotes) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(`The \`Release Notes\` section seems to be missing. Please check if the section header in MR description is present and in the correct markdown format ("## Release Notes").\n\nSee [Release Notes Format Rules](${wiki_link}).`);
|
||||
}
|
||||
|
||||
const releaseNotesLines = sectionReleaseNotes[1].replace(/<!--[\s\S]*?-->/g, '')
|
||||
|
||||
const lines = releaseNotesLines.split("\n").filter(s => s.trim().length > 0);
|
||||
let valid_entries_found = 0;
|
||||
let no_release_notes_found = false;
|
||||
let violations = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.match(regexValidEntry)) {
|
||||
valid_entries_found++;
|
||||
const error_msg = check_entry(line);
|
||||
if (error_msg) {
|
||||
violations.push(error_msg);
|
||||
}
|
||||
} else if (line.match(regexNoReleaseNotes)) {
|
||||
no_release_notes_found = true;
|
||||
}
|
||||
});
|
||||
|
||||
let error_output = [];
|
||||
if (violations.length > 0) {
|
||||
error_output = [...error_output, 'Invalid release note entries:', violations.join('\n')];
|
||||
}
|
||||
if (no_release_notes_found) {
|
||||
if (valid_entries_found > 0) {
|
||||
error_output.push('`No release notes` comment shows up when there is valid entry. Remove bullets before comments in release notes section.');
|
||||
}
|
||||
} else {
|
||||
if (!valid_entries_found) {
|
||||
error_output.push('The `Release Notes` section seems to have no valid entries. Add bullets before valid entries, or add `No release notes` comment to suppress this error if you mean to have no release notes.');
|
||||
}
|
||||
}
|
||||
|
||||
if (error_output.length > 0) {
|
||||
// Paragraphs joined by double `\n`s.
|
||||
error_output = [...error_output, `See [Release Notes Format Guide](${wiki_link}).`].join('\n\n');
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(error_output);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, 'Passed');
|
||||
};
|
||||
|
||||
function check_entry(entry) {
|
||||
const entry_str = `- \`${entry}\``;
|
||||
const indent = " ";
|
||||
|
||||
if (entry.match(/no\s+release\s+note/i)) {
|
||||
return [entry_str, `${indent}- \`No release notes\` comment shouldn't start with bullet.`].join('\n');
|
||||
}
|
||||
|
||||
// Remove a leading escaping backslash of the special characters, https://www.markdownguide.org/basic-syntax/#characters-you-can-escape
|
||||
const escapeCharRegex = /\\([\\`*_{}[\]<>()+#-.!|])/g;
|
||||
entry = entry.replace(escapeCharRegex, '$1');
|
||||
|
||||
const regex = /^(\s*)[-*+]\s+\[([^\]]+)\]\s+(.*)$/;
|
||||
const match = regex.exec(entry);
|
||||
if (!match) {
|
||||
return [entry_str, `${indent}- Please specify the [area] to which the change belongs (see guide). If this line is just a comment, remove the bullet.`].join('\n');
|
||||
}
|
||||
|
||||
// area is in match[2]
|
||||
const description = match[3].trim();
|
||||
let violations = [];
|
||||
|
||||
if (match[1]) {
|
||||
violations.push(`${indent}- Release note entry should start from the beginning of line. (Nested release note not allowed.)`);
|
||||
}
|
||||
|
||||
if (!/^[A-Z0-9]/.test(description)) {
|
||||
violations.push(`${indent}- Release note statement should start with a capital letter or digit.`);
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
return [entry_str, ...violations].join('\n');
|
||||
}
|
||||
return null;
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check the documentation files in this MR.
|
||||
*
|
||||
* Generate an object with all docs/ files found in this MR with paths to their EN/CN versions.
|
||||
*
|
||||
* For common files (both language versions exist in this MR), compare the lines of both files.
|
||||
* Ignore if the CN file is only a single line file with an "include" reference to the EN version.
|
||||
*
|
||||
* For files that only have a CN version in this MR, add a message to the message that an EN file also needs to be created.
|
||||
*
|
||||
* For a file that only has an EN version in this MR, try loading its CN version from the target Gitlab branch.
|
||||
* If its CN version doesn't exist in the repository or it does exist,
|
||||
* but its contents are larger than just an "include" link to the EN version (it's a full-size file),
|
||||
* add a message to the report
|
||||
*
|
||||
* Create a compiled report with the docs/ files issues found and set its severity (WARN/INFO).
|
||||
* Severity is based on the presence of "needs translation: ??" labels in this MR
|
||||
*
|
||||
* @dangerjs WARN (if docs translation issues in the MR)
|
||||
* @dangerjs INFO (if docs translation issues in the MR and the user has already added translation labels).
|
||||
* Adding translation labels "needs translation: XX" automatically notifies the Documentation team
|
||||
*
|
||||
* @dangerjs WARN (if there are no docs issues in MR, but translation labels have been added anyway)
|
||||
*
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const ruleName = 'Documentation translation';
|
||||
let partMessages = []; // Create a blank field for future records of individual issues
|
||||
const pathProject = "espressif/esp-idf";
|
||||
const regexIncludeLink = /\.\.\sinclude::\s((\.\.\/)+)en\//;
|
||||
const allMrFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files,
|
||||
...danger.git.deleted_files,
|
||||
];
|
||||
|
||||
const docsFilesMR = parseMrDocsFiles(allMrFiles); // Create single object of all doc files in MR with names, paths and groups
|
||||
|
||||
// Both versions (EN and CN) of document found changed in this MR
|
||||
for (const file of docsFilesMR.bothFilesInMr) {
|
||||
file.contentEn = await getContentFileInMR(file.fileEnPath); // Get content of English file
|
||||
file.linesEn = file.contentEn.split("\n").length; // Get number of lines of English file
|
||||
|
||||
file.contentCn = await getContentFileInMR(file.fileCnPath); // Get content of Chinese file
|
||||
file.linesCn = file.contentCn.split("\n").length; // Get number of lines of English file
|
||||
|
||||
// Compare number of lines in both versions
|
||||
if (file.linesEn !== file.linesCn) {
|
||||
// Check if CN file is only link to EN file
|
||||
if (!regexIncludeLink.test(file.contentCn)) {
|
||||
// if not just a link ...
|
||||
partMessages.push(
|
||||
`- please synchronize the EN and CN version of \`${file.fileName}\`. [\`${file.fileEnPath}\`](${file.fileUrlRepoEN}) has ${file.linesEn} lines; [\`${file.fileCnPath}\`](${file.fileUrlRepoCN}) has ${file.linesCn} lines.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only Chinese version of document found changed in this MR
|
||||
for (const file of docsFilesMR.onlyCnFilesInMr) {
|
||||
partMessages.push(
|
||||
`- file \`${file.fileEnPath}\` doesn't exist in this MR or in the GitLab repo. Please add \`${file.fileEnPath}\` into this MR.`
|
||||
);
|
||||
}
|
||||
|
||||
// Only English version of document found in this MR
|
||||
for (const file of docsFilesMR.onlyEnFilesInMr) {
|
||||
const targetBranch = danger.gitlab.mr.target_branch;
|
||||
file.contentCn = await getContentFileInGitlab(
|
||||
file.fileCnPath,
|
||||
targetBranch
|
||||
); // Try to fetch CN file from target branch of Gitlab repository and store content
|
||||
|
||||
if (file.contentCn) {
|
||||
// File found on target branch in Gitlab repository
|
||||
if (!regexIncludeLink.test(file.contentCn)) {
|
||||
// File on Gitlab master is NOT just an ..include:: link to ENG version
|
||||
file.fileUrlRepoMasterCN = `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${targetBranch}/${file.fileCnPath}`;
|
||||
partMessages.push(
|
||||
`- file \`${file.fileCnPath}\` was not updated in this MR, but found unchanged full document (not just link to EN) in target branch of Gitlab repository [\`${file.fileCnPath}\`](${file.fileUrlRepoMasterCN}). Please update \`${file.fileCnPath}\` into this MR.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// File failed to fetch, probably does not exist in the target branch
|
||||
partMessages.push(
|
||||
`- file \`${file.fileCnPath}\` probably doesn't exist in this MR or in the GitLab repo. Please add \`${file.fileCnPath}\` into this MR.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a report with found issues with documents in MR
|
||||
createReport();
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, 'Passed');
|
||||
|
||||
/**
|
||||
* Generates an object that represents the relationships between files in two different languages found in this MR.
|
||||
*
|
||||
* @param {string[]} docsFilesEN - An array of file paths for documents in English.
|
||||
* @param {string[]} docsFilesCN - An array of file paths for documents in Chinese.
|
||||
* @returns {Object} An object with the following properties:
|
||||
* - bothFilesInMr: An array of objects that represent files that found in MR in both languages. Each object has the following properties:
|
||||
* - fileName: The name of the file.
|
||||
* - fileEnPath: The path to the file in English.
|
||||
* - fileCnPath: The path to the file in Chinese.
|
||||
* - fileUrlRepoEN: The URL link to MR branch path to the file in English.
|
||||
* - fileUrlRepoCN: The URL link to MR branch path to the file in Chinese.
|
||||
* - onlyCnFilesInMr: An array of objects that represent files that only found in MR in English. Each object has the following properties:
|
||||
* - fileName: The name of the file.
|
||||
* - fileEnPath: The path to the file in English.
|
||||
* - fileCnPath: The FUTURE path to the file in Chinese.
|
||||
* - fileUrlRepoEN: The URL link to MR branch path to the file in English.
|
||||
* - fileUrlRepoCN: The URL link to MR branch path to the file in Chinese.
|
||||
* - onlyEnFilesInMr: An array of objects that represent files that only found in MR in Chinese. Each object has the following properties:
|
||||
* - fileName: The name of the file.
|
||||
* - fileEnPath: The FUTURE path to the file in English.
|
||||
* - fileCnPath: The path to the file in Chinese.
|
||||
* - fileUrlRepoEN: The URL link to MR branch path to the file in English.
|
||||
* - fileUrlRepoCN: The URL link to MR branch path to the file in Chinese.
|
||||
*/
|
||||
function parseMrDocsFiles(allMrFiles) {
|
||||
const path = require("path");
|
||||
const mrBranch = danger.gitlab.mr.source_branch;
|
||||
|
||||
const docsEnFilesMrPath = allMrFiles.filter((file) =>
|
||||
file.startsWith("docs/en")
|
||||
); // Filter all English doc files in MR
|
||||
const docsCnFilesMrPath = allMrFiles.filter((file) =>
|
||||
file.startsWith("docs/zh_CN")
|
||||
); // Filter all Chinese doc files in MR
|
||||
|
||||
const docsEnFileNames = docsEnFilesMrPath.map((filePath) =>
|
||||
path.basename(filePath)
|
||||
); // Get (base) file names for English docs
|
||||
const docsCnFileNames = docsCnFilesMrPath.map((filePath) =>
|
||||
path.basename(filePath)
|
||||
); // Get (base) file names for Chinese docs
|
||||
|
||||
const bothFileNames = docsEnFileNames.filter((fileName) =>
|
||||
docsCnFileNames.includes(fileName)
|
||||
); // Get file names that are common to both English and Chinese docs
|
||||
const onlyEnFileNames = docsEnFileNames.filter(
|
||||
(fileName) => !docsCnFileNames.includes(fileName)
|
||||
); // Get file names that are only present in English version
|
||||
const onlyCnFileNames = docsCnFileNames.filter(
|
||||
(fileName) => !docsEnFileNames.includes(fileName)
|
||||
); // Get file names that are only present in Chinese version
|
||||
|
||||
return {
|
||||
bothFilesInMr: bothFileNames.map((fileName) => {
|
||||
const fileEnPath =
|
||||
docsEnFilesMrPath[docsEnFileNames.indexOf(fileName)];
|
||||
const fileCnPath =
|
||||
docsCnFilesMrPath[docsCnFileNames.indexOf(fileName)];
|
||||
|
||||
return {
|
||||
fileName,
|
||||
fileEnPath,
|
||||
fileCnPath,
|
||||
fileUrlRepoEN: `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${mrBranch}/${fileEnPath}`,
|
||||
fileUrlRepoCN: `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${mrBranch}/${fileCnPath}`,
|
||||
};
|
||||
}),
|
||||
onlyEnFilesInMr: onlyEnFileNames.map((fileName) => {
|
||||
const fileEnPath =
|
||||
docsEnFilesMrPath[docsEnFileNames.indexOf(fileName)];
|
||||
const fileCnPath = fileEnPath.replace("en", "zh_CN"); // Generate future CN file path
|
||||
|
||||
return {
|
||||
fileName,
|
||||
fileEnPath,
|
||||
fileCnPath,
|
||||
fileUrlRepoEN: `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${mrBranch}/${fileEnPath}`,
|
||||
fileUrlRepoCN: `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${mrBranch}/${fileCnPath}`,
|
||||
};
|
||||
}),
|
||||
onlyCnFilesInMr: onlyCnFileNames.map((fileName) => {
|
||||
const fileCnPath =
|
||||
docsCnFilesMrPath[docsCnFileNames.indexOf(fileName)];
|
||||
const fileEnPath = fileCnPath.replace("zh_CN", "en"); // Generate future EN file path
|
||||
|
||||
return {
|
||||
fileName,
|
||||
fileEnPath,
|
||||
fileCnPath,
|
||||
fileUrlRepoEN: `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${mrBranch}/${fileEnPath}`,
|
||||
fileUrlRepoCN: `${process.env.DANGER_GITLAB_HOST}/${pathProject}/-/blob/${mrBranch}/${fileCnPath}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contents of a file from GitLab using the GitLab API.
|
||||
*
|
||||
* @param {string} filePath - The path of the file to retrieve.
|
||||
* @param {string} branch - The branch where the file is located.
|
||||
* @returns {string|null} - The contents of the file, with any trailing new lines trimmed, or null if the file cannot be retrieved.
|
||||
*/
|
||||
async function getContentFileInGitlab(filePath, branch) {
|
||||
const axios = require("axios");
|
||||
|
||||
const encFilePath = encodeURIComponent(filePath);
|
||||
const encBranch = encodeURIComponent(branch);
|
||||
const urlApi = `${process.env.DANGER_GITLAB_API_BASE_URL}/projects/${danger.gitlab.mr.project_id}/repository/files/${encFilePath}/raw?ref=${encBranch}`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(urlApi, {
|
||||
headers: {
|
||||
"Private-Token": process.env.DANGER_GITLAB_API_TOKEN,
|
||||
},
|
||||
});
|
||||
return response.data.trim(); // Trim trailing new line
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contents of a file in a DangerJS merge request object.
|
||||
*
|
||||
* @param {string} filePath - The path of the file to retrieve.
|
||||
* @returns {string|null} - The contents of the file, with any trailing new lines trimmed, or null if the file cannot be retrieved.
|
||||
*/
|
||||
async function getContentFileInMR(filePath) {
|
||||
try {
|
||||
const content = await danger.git.diffForFile(filePath);
|
||||
const fileContentAfter = content.after.trim(); // Trim trailing new lines
|
||||
return fileContentAfter;
|
||||
} catch (error) {
|
||||
console.error(`Error while getting file content MR: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a compiled report for found documentation issues in the current MR and alerts the Documentation team if there are any "needs translation" labels present.
|
||||
*
|
||||
* Report if documentation labels have been added by mistake.
|
||||
*/
|
||||
function createReport() {
|
||||
const mrLabels = danger.gitlab.mr.labels; // Get MR labels
|
||||
const regexTranslationLabel = /needs translation:/i;
|
||||
|
||||
const translationLabelsPresent = mrLabels.some((label) =>
|
||||
regexTranslationLabel.test(label)
|
||||
); // Check if any of MR labels are "needs translation: XX"
|
||||
|
||||
// No docs issues found in MR, but translation labels have been added anyway
|
||||
if (!partMessages.length && translationLabelsPresent) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(
|
||||
`Please remove the \`needs translation: XX\` labels. For documents that need to translate from scratch, Doc team will translate them in the future. For the current stage, we only focus on updating exiting EN and CN translation to make them in sync.`
|
||||
);
|
||||
}
|
||||
|
||||
// Docs issues found in this MR
|
||||
partMessages.sort();
|
||||
let dangerMessage = `Some of the documentation files in this MR seem to have translations issues:\n${partMessages.join(
|
||||
"\n"
|
||||
)}\n`;
|
||||
|
||||
if (partMessages.length) {
|
||||
if (!translationLabelsPresent) {
|
||||
dangerMessage += `
|
||||
\nWhen synchronizing the EN and CN versions, please follow the [Documentation Code](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/contribute/documenting-code.html#standardize-document-format). The total number of lines of EN and CN should be same.\n
|
||||
\nIf you have difficulty in providing translation, you can contact Documentation team by adding <kbd>needs translation: CN</kbd> or <kbd>needs translation: EN</kbd> labels into this MR and retrying Danger CI job. The documentation team will be automatically notified and will help you with the translations before the merge.\n`;
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(dangerMessage); // no "needs translation: XX" labels in MR; report issues as warn
|
||||
} else {
|
||||
dangerMessage += `\nTranslation labels <kbd>needs translation: CN</kbd> or <kbd>needs translation: EN</kbd> were added - this will automatically notify the Documentation team to help you with translation issues.`;
|
||||
recordRuleExitStatus(ruleName, 'Passed (with suggestions)');
|
||||
return message(dangerMessage); // "needs translation: XX" labels were found in MR and Docs team was notified; report issues as info
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if MR is too large (more than 1000 lines of changes)
|
||||
*
|
||||
* @dangerjs INFO
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const ruleName = "Merge request size (number of changed lines)";
|
||||
const bigMrLinesOfCodeThreshold = 1000;
|
||||
const totalLines = await danger.git.linesOfCode();
|
||||
|
||||
if (totalLines > bigMrLinesOfCodeThreshold) {
|
||||
recordRuleExitStatus(ruleName, "Passed (with suggestions)");
|
||||
return message(
|
||||
`This MR seems to be quite large (total lines of code: ${totalLines}), you might consider splitting it into smaller MRs`
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, "Passed");
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Throw Danger WARN if branch name contains more than one slash or uppercase letters
|
||||
*
|
||||
* @dangerjs INFO
|
||||
*/
|
||||
module.exports = function () {
|
||||
const ruleName = "Source branch name";
|
||||
const sourceBranch = danger.gitlab.mr.source_branch;
|
||||
|
||||
// Check if the source branch name contains more than one slash
|
||||
const slashCount = (sourceBranch.match(/\//g) || []).length;
|
||||
if (slashCount > 1) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(
|
||||
`The source branch name \`${sourceBranch}\` contains more than one slash. This can cause troubles with git sync. Please rename the branch.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the source branch name contains any uppercase letters
|
||||
if (sourceBranch !== sourceBranch.toLowerCase()) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(
|
||||
`The source branch name \`${sourceBranch}\` contains uppercase letters. This can cause troubles on case-insensitive file systems (macOS). Please use only lowercase letters.`
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, "Passed");
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
const { recordRuleExitStatus } = require("./configParameters.js");
|
||||
|
||||
/**
|
||||
* Check if MR Title contains prefix "WIP: ...".
|
||||
*
|
||||
* @dangerjs WARN
|
||||
*/
|
||||
module.exports = function () {
|
||||
const ruleName = 'Merge request not in Draft or WIP state';
|
||||
const mrTitle = danger.gitlab.mr.title;
|
||||
const regexes = [
|
||||
{ prefix: "WIP", regex: /^WIP:/i },
|
||||
{ prefix: "W.I.P", regex: /^W\.I\.P/i },
|
||||
{ prefix: "[WIP]", regex: /^\[WIP/i },
|
||||
{ prefix: "[W.I.P]", regex: /^\[W\.I\.P/i },
|
||||
{ prefix: "(WIP)", regex: /^\(WIP/i },
|
||||
{ prefix: "(W.I.P)", regex: /^\(W\.I\.P/i },
|
||||
];
|
||||
|
||||
for (const item of regexes) {
|
||||
if (item.regex.test(mrTitle)) {
|
||||
recordRuleExitStatus(ruleName, "Failed");
|
||||
return warn(
|
||||
`Please remove the \`${item.prefix}\` prefix from the MR name before merging this MR.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the rule has passed
|
||||
recordRuleExitStatus(ruleName, "Passed");
|
||||
};
|
2745
.gitlab/dangerjs/package-lock.json
generated
2745
.gitlab/dangerjs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "dangerjs-esp-idf",
|
||||
"description": "Merge request automatic linter",
|
||||
"main": "dangerfile.js",
|
||||
"dependencies": {
|
||||
"danger": "^11.2.3",
|
||||
"axios": "^1.3.3",
|
||||
"langchain": "^0.0.53",
|
||||
"openai-gpt-token-counter": "^1.0.3",
|
||||
"@commitlint/lint": "^13.1.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user