Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions danger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ The Danger action runs the following checks:
- **Action pinning**: Verifies GitHub Actions are pinned to specific commits for security
- **Conventional commits**: Validates commit message format and PR title conventions
- **Cross-repo links**: Checks for proper formatting of links in changelog entries
- **Legal boilerplate validation**: For external contributors (non-organization members), verifies the presence of required legal notices in PR descriptions when the repository's PR template includes a "Legal Boilerplate" section

For detailed rule implementations, see [dangerfile.js](dangerfile.js).

Expand Down
145 changes: 144 additions & 1 deletion danger/dangerfile-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,151 @@ function extractPRFlavor(prTitle, prBranchRef) {
return "";
}

/**
* Extract the legal boilerplate section from the PR template
* @param {string} templateContent - The PR template content
* @returns {string} The extracted legal boilerplate section
*/
function extractLegalBoilerplateSection(templateContent) {
// Find the legal boilerplate section and extract it
const lines = templateContent.split('\n');
let inLegalSection = false;
let legalSection = [];

for (let i = 0; i < lines.length; i++) {
const line = lines[i];

// Check if this line is the legal boilerplate header
if (/^#{1,6}\s+Legal\s+Boilerplate/i.test(line)) {
inLegalSection = true;
legalSection.push(line);
continue;
}

// If we're in the legal section
if (inLegalSection) {
// Check if we've reached another header (end of legal section)
if (/^#{1,6}\s+/.test(line)) {
break;
}
legalSection.push(line);
}
}

return legalSection.join('\n').trim();
}

/**
* Check that external contributors include the required legal boilerplate in their PR body.
* Accepts danger context and reporting functions as parameters for testability.
*
* @param {object} options
* @param {object} options.danger - The DangerJS danger object
* @param {Function} options.fail - DangerJS fail function
* @param {Function} options.markdown - DangerJS markdown function
*/
async function checkLegalBoilerplate({ danger, fail, markdown }) {
console.log('::debug:: Checking legal boilerplate requirements...');

// Check if the PR author is an external contributor using author_association
const authorAssociation = danger.github.pr.author_association;
const isExternalContributor = !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation);

if (!isExternalContributor) {
console.log('::debug:: Skipping legal boilerplate check for organization member/collaborator');
return;
}

// Find PR template
let prTemplateContent = null;
const possibleTemplatePaths = [
'.github/PULL_REQUEST_TEMPLATE.md',
'.github/pull_request_template.md',
'PULL_REQUEST_TEMPLATE.md',
'pull_request_template.md',
'.github/PULL_REQUEST_TEMPLATE/pull_request_template.md'
];

for (const templatePath of possibleTemplatePaths) {
const content = await danger.github.utils.fileContents(templatePath);
if (content) {
prTemplateContent = content;
console.log(`::debug:: Found PR template at ${templatePath}`);
break;
}
}

if (!prTemplateContent) {
console.log('::debug:: No PR template found, skipping legal boilerplate check');
return;
}

// Check if template contains a Legal Boilerplate section
const legalBoilerplateHeaderRegex = /^#{1,6}\s+Legal\s+Boilerplate/im;
if (!legalBoilerplateHeaderRegex.test(prTemplateContent)) {
console.log('::debug:: PR template does not contain a Legal Boilerplate section');
return;
}

// Extract expected boilerplate from template
const expectedBoilerplate = extractLegalBoilerplateSection(prTemplateContent);
const prBody = danger.github.pr.body || '';

// Extract actual boilerplate from PR body
const actualBoilerplate = extractLegalBoilerplateSection(prBody);

// Check if PR body contains the legal boilerplate section
if (!actualBoilerplate) {
fail('This PR is missing the required legal boilerplate. As an external contributor, please include the "Legal Boilerplate" section from the PR template in your PR description.');

markdown(`
### ⚖️ Legal Boilerplate Required

As an external contributor, your PR must include the legal boilerplate from the PR template.

Please add the following section to your PR description:

\`\`\`markdown
${expectedBoilerplate}
\`\`\`

This is required to ensure proper intellectual property rights for your contributions.
`.trim());
return;
}

// Verify the actual boilerplate matches the expected one
// Normalize whitespace for comparison
const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim();
const expectedNormalized = normalizeWhitespace(expectedBoilerplate);
const actualNormalized = normalizeWhitespace(actualBoilerplate);

if (expectedNormalized !== actualNormalized) {
fail('The legal boilerplate in your PR description does not match the template. Please ensure you include the complete, unmodified legal text from the PR template.');

markdown(`
### ⚖️ Legal Boilerplate Mismatch

Your PR contains a "Legal Boilerplate" section, but it doesn't match the required text from the template.

Please replace it with the exact text from the template:

\`\`\`markdown
${expectedBoilerplate}
\`\`\`

This is required to ensure proper intellectual property rights for your contributions.
`.trim());
return;
}

console.log('::debug:: Legal boilerplate validated successfully ✓');
}

module.exports = {
FLAVOR_CONFIG,
getFlavorConfig,
extractPRFlavor
extractPRFlavor,
extractLegalBoilerplateSection,
checkLegalBoilerplate
};
Loading
Loading