Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
37 changes: 36 additions & 1 deletion danger/dangerfile-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,43 @@ 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();
}

module.exports = {
FLAVOR_CONFIG,
getFlavorConfig,
extractPRFlavor
extractPRFlavor,
extractLegalBoilerplateSection
};
248 changes: 247 additions & 1 deletion danger/dangerfile-utils.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { getFlavorConfig, extractPRFlavor, FLAVOR_CONFIG } = require('./dangerfile-utils.js');
const { getFlavorConfig, extractPRFlavor, extractLegalBoilerplateSection, FLAVOR_CONFIG } = require('./dangerfile-utils.js');

describe('dangerfile-utils', () => {
describe('getFlavorConfig', () => {
Expand Down Expand Up @@ -275,4 +275,250 @@ describe('dangerfile-utils', () => {
});
});
});

describe('extractLegalBoilerplateSection', () => {
it('should extract legal boilerplate section with ### header', () => {
const template = `
# Pull Request Template

## Description
Please describe your changes

### Legal Boilerplate
Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.

## Checklist
- [ ] Tests added
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('### Legal Boilerplate'), 'Should include the header');
assert.ok(result.includes('Functional Software, Inc.'), 'Should include the legal text');
assert.ok(!result.includes('## Checklist'), 'Should not include the next section');
});

it('should extract legal boilerplate section with ## header', () => {
const template = `
# Pull Request Template

## Legal Boilerplate

This is a legal notice.

## Other Section
More content
`;

const result = extractLegalBoilerplateSection(template);

assert.strictEqual(result.trim(), '## Legal Boilerplate\n\nThis is a legal notice.');
});

it('should extract legal boilerplate section with different heading levels', () => {
const testCases = [
{ header: '# Legal Boilerplate', text: 'Level 1 header' },
{ header: '## Legal Boilerplate', text: 'Level 2 header' },
{ header: '### Legal Boilerplate', text: 'Level 3 header' },
{ header: '#### Legal Boilerplate', text: 'Level 4 header' },
{ header: '##### Legal Boilerplate', text: 'Level 5 header' },
{ header: '###### Legal Boilerplate', text: 'Level 6 header' }
];

testCases.forEach(({ header, text }) => {
const template = `${header}\n${text}\n## Next Section`;
const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes(header), `Should extract section with ${header}`);
assert.ok(result.includes(text), `Should include text for ${header}`);
assert.ok(!result.includes('Next Section'), `Should not include next section for ${header}`);
});
});

it('should handle case-insensitive matching', () => {
const templates = [
'### Legal Boilerplate\nContent',
'### legal boilerplate\nContent',
'### LEGAL BOILERPLATE\nContent',
'### Legal BOILERPLATE\nContent'
];

templates.forEach(template => {
const result = extractLegalBoilerplateSection(template);
assert.ok(result.length > 0, `Should extract from: ${template.split('\n')[0]}`);
assert.ok(result.includes('Content'), `Should include content from: ${template.split('\n')[0]}`);
});
});

it('should handle legal boilerplate with multiple paragraphs', () => {
const template = `
### Legal Boilerplate

First paragraph of legal text.

Second paragraph of legal text.

Third paragraph of legal text.

## Next Section
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('First paragraph'), 'Should include first paragraph');
assert.ok(result.includes('Second paragraph'), 'Should include second paragraph');
assert.ok(result.includes('Third paragraph'), 'Should include third paragraph');
assert.ok(!result.includes('Next Section'), 'Should not include next section');
});

it('should handle legal boilerplate at end of template', () => {
const template = `
# PR Template

## Description
Content

### Legal Boilerplate
Legal text at the end.
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('### Legal Boilerplate'), 'Should include header');
assert.ok(result.includes('Legal text at the end.'), 'Should include text');
});

it('should return empty string when no legal boilerplate section exists', () => {
const template = `
# Pull Request Template

## Description
Please describe your changes

## Checklist
- [ ] Tests added
`;

const result = extractLegalBoilerplateSection(template);

assert.strictEqual(result, '', 'Should return empty string when no legal section found');
});

it('should handle empty template', () => {
const result = extractLegalBoilerplateSection('');
assert.strictEqual(result, '', 'Should return empty string for empty template');
});

it('should handle template with only legal boilerplate section', () => {
const template = '### Legal Boilerplate\nThis is the only content.';
const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('### Legal Boilerplate'), 'Should include header');
assert.ok(result.includes('This is the only content.'), 'Should include content');
});

it('should handle legal boilerplate with special characters', () => {
const template = `
### Legal Boilerplate
Text with special chars: @#$%^&*()_+-={}[]|\\:";'<>?,./
And some unicode: 你好世界 🎉

## Next
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('special chars'), 'Should handle special characters');
assert.ok(result.includes('你好世界'), 'Should handle unicode');
assert.ok(result.includes('🎉'), 'Should handle emoji');
});

it('should handle legal boilerplate with code blocks', () => {
const template = `
### Legal Boilerplate

Some text with code:

\`\`\`javascript
const legal = true;
\`\`\`

More text.

## Next Section
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('const legal = true;'), 'Should include code blocks');
assert.ok(result.includes('More text.'), 'Should include text after code block');
assert.ok(!result.includes('Next Section'), 'Should not include next section');
});

it('should handle legal boilerplate with lists', () => {
const template = `
### Legal Boilerplate

You agree to:
- Item 1
- Item 2
- Item 3

## Other
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('- Item 1'), 'Should include list items');
assert.ok(result.includes('- Item 2'), 'Should include list items');
assert.ok(result.includes('- Item 3'), 'Should include list items');
});

it('should handle legal boilerplate with extra whitespace', () => {
const template = `
### Legal Boilerplate
Content with spaces.
## Next
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('Content with spaces.'), 'Should handle extra whitespace in header');
});

it('should stop at first subsequent header', () => {
const template = `
### Legal Boilerplate
First section content.
### Another Legal Boilerplate
This should not be included.
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('First section content.'), 'Should include first section');
assert.ok(!result.includes('This should not be included.'), 'Should stop at next header');
});

it('should handle blank lines within legal section', () => {
const template = `
### Legal Boilerplate

First paragraph.


Second paragraph with blank lines above.

## Next
`;

const result = extractLegalBoilerplateSection(template);

assert.ok(result.includes('First paragraph.'), 'Should include first paragraph');
assert.ok(result.includes('Second paragraph'), 'Should include second paragraph');
// Should preserve blank lines
const blankLineCount = (result.match(/\n\n/g) || []).length;
assert.ok(blankLineCount >= 1, 'Should preserve blank lines');
});
});
});
Loading