Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Features

- Danger - Add legal boilerplate validation for external contributors ([#145](https://github.com/getsentry/github-workflows/pull/145))
- Verify that PRs from non-organization members include the required legal boilerplate from the PR template
- Show actionable markdown hints when the boilerplate is missing or doesn't match

### Fixes

- Sentry-CLI integration test action - Accept chunked ProGuard uploads for compatibility with Sentry CLI 3.x ([#140](https://github.com/getsentry/github-workflows/pull/140))
Expand Down
11 changes: 11 additions & 0 deletions danger/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Contributing

## Running tests

The test suite uses Node.js's built-in test runner (requires Node 18+):

```sh
cd danger
node --test
```

No dependencies to install — the tests use only `node:test` and `node:assert`.

## How to run dangerfile locally

- [Working on your Dangerfile](https://danger.systems/js/guides/the_dangerfile.html#working-on-your-dangerfile)
Expand Down
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
116 changes: 115 additions & 1 deletion danger/dangerfile-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,122 @@ function extractPRFlavor(prTitle, prBranchRef) {
return "";
}

/** @returns {string} The legal boilerplate section extracted from the content, or empty string if none found */
function extractLegalBoilerplateSection(content) {
const lines = content.split('\n');
const legalHeaderIndex = lines.findIndex(line => /^#{1,6}\s+Legal\s+Boilerplate/i.test(line));

if (legalHeaderIndex === -1) {
return '';
}

const sectionLines = [lines[legalHeaderIndex]];

for (let i = legalHeaderIndex + 1; i < lines.length; i++) {
if (/^#{1,6}\s+/.test(lines[i])) {
break;
}
sectionLines.push(lines[i]);
}

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

const INTERNAL_ASSOCIATIONS = ['OWNER', 'MEMBER', 'COLLABORATOR'];

const PR_TEMPLATE_PATHS = [
'.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'
];

/// Collapse all whitespace runs into single spaces for comparison.
function normalizeWhitespace(str) {
return str.replace(/\s+/g, ' ').trim();
}

/// Try each known PR template path and return the first one with content.
async function findPRTemplate(danger) {
for (const templatePath of PR_TEMPLATE_PATHS) {
const content = await danger.github.utils.fileContents(templatePath);
if (content) {
console.log(`::debug:: Found PR template at ${templatePath}`);
return content;
}
}
return null;
}

/// Build a markdown hint showing the expected boilerplate text.
function formatBoilerplateHint(title, description, expectedBoilerplate) {
return `### ⚖️ ${title}

${description}

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

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

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

const authorAssociation = danger.github.pr.author_association;
console.log(`::debug:: PR author_association: ${authorAssociation}`);
if (INTERNAL_ASSOCIATIONS.includes(authorAssociation)) {
console.log('::debug:: Skipping legal boilerplate check for organization member/collaborator');
return;
}

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

const expectedBoilerplate = extractLegalBoilerplateSection(prTemplateContent);
if (!expectedBoilerplate) {
console.log('::debug:: PR template does not contain a Legal Boilerplate section');
return;
}

const actualBoilerplate = extractLegalBoilerplateSection(danger.github.pr.body || '');

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(formatBoilerplateHint(
'Legal Boilerplate Required',
'As an external contributor, your PR must include the legal boilerplate from the PR template.\n\nPlease add the following section to your PR description:',
expectedBoilerplate
));
return;
}

if (normalizeWhitespace(expectedBoilerplate) !== normalizeWhitespace(actualBoilerplate)) {
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(formatBoilerplateHint(
'Legal Boilerplate Mismatch',
'Your PR contains a "Legal Boilerplate" section, but it doesn\'t match the required text from the template.\n\nPlease replace it with the exact text from the template:',
expectedBoilerplate
));
return;
}

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

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