Skip to content

Commit 2f7e57b

Browse files
Copilotvaind
andcommitted
Add legal boilerplate validation for external contributors
Co-authored-by: vaind <6349682+vaind@users.noreply.github.com>
1 parent 983773a commit 2f7e57b

2 files changed

Lines changed: 132 additions & 0 deletions

File tree

danger/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ The Danger action runs the following checks:
6161
- **Action pinning**: Verifies GitHub Actions are pinned to specific commits for security
6262
- **Conventional commits**: Validates commit message format and PR title conventions
6363
- **Cross-repo links**: Checks for proper formatting of links in changelog entries
64+
- **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
6465

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

danger/dangerfile.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,136 @@ async function checkActionsArePinned() {
186186
}
187187
}
188188

189+
async function checkLegalBoilerplate() {
190+
console.log('::debug:: Checking legal boilerplate requirements...');
191+
192+
// Check if the PR author is an external contributor
193+
const prAuthor = danger.github.pr.user.login;
194+
const repoOwner = danger.github.pr.base.repo.owner.login;
195+
196+
// Check if author is a member of the getsentry organization
197+
let isExternalContributor = false;
198+
try {
199+
// Check organization membership
200+
await danger.github.api.orgs.checkMembershipForUser({
201+
org: repoOwner,
202+
username: prAuthor
203+
});
204+
console.log(`::debug:: ${prAuthor} is a member of ${repoOwner} organization`);
205+
isExternalContributor = false;
206+
} catch (error) {
207+
// If the API call fails with 404, the user is not a member
208+
if (error.status === 404) {
209+
console.log(`::debug:: ${prAuthor} is NOT a member of ${repoOwner} organization`);
210+
isExternalContributor = true;
211+
} else {
212+
// For other errors (like permission issues), log and skip the check
213+
console.log(`::warning:: Could not check organization membership for ${prAuthor}: ${error.message}`);
214+
return;
215+
}
216+
}
217+
218+
// If not an external contributor, skip the check
219+
if (!isExternalContributor) {
220+
console.log('::debug:: Skipping legal boilerplate check for organization member');
221+
return;
222+
}
223+
224+
// Check if the PR template contains a legal boilerplate section
225+
let prTemplateContent = null;
226+
const possibleTemplatePaths = [
227+
'.github/PULL_REQUEST_TEMPLATE.md',
228+
'.github/pull_request_template.md',
229+
'PULL_REQUEST_TEMPLATE.md',
230+
'pull_request_template.md',
231+
'.github/PULL_REQUEST_TEMPLATE/pull_request_template.md'
232+
];
233+
234+
for (const templatePath of possibleTemplatePaths) {
235+
try {
236+
prTemplateContent = await danger.github.utils.fileContents(templatePath);
237+
console.log(`::debug:: Found PR template at ${templatePath}`);
238+
break;
239+
} catch (error) {
240+
// Template not found at this path, try next
241+
continue;
242+
}
243+
}
244+
245+
// If no template found, skip the check
246+
if (!prTemplateContent) {
247+
console.log('::debug:: No PR template found, skipping legal boilerplate check');
248+
return;
249+
}
250+
251+
// Check if the template contains a legal boilerplate section
252+
// Look for headers like "### Legal Boilerplate", "## Legal Boilerplate", etc.
253+
const legalBoilerplateHeaderRegex = /^#{1,6}\s+Legal\s+Boilerplate/im;
254+
if (!legalBoilerplateHeaderRegex.test(prTemplateContent)) {
255+
console.log('::debug:: PR template does not contain a Legal Boilerplate section');
256+
return;
257+
}
258+
259+
console.log('::debug:: PR template contains Legal Boilerplate section, checking PR description...');
260+
261+
// Check if the PR description contains the legal boilerplate
262+
const prBody = danger.github.pr.body || '';
263+
264+
// Look for the legal boilerplate header in the PR description
265+
if (!legalBoilerplateHeaderRegex.test(prBody)) {
266+
fail(
267+
'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.',
268+
undefined,
269+
undefined
270+
);
271+
272+
markdown(`
273+
### ⚖️ Legal Boilerplate Required
274+
275+
As an external contributor, your PR must include the legal boilerplate from the PR template.
276+
277+
Please add the following section to your PR description:
278+
279+
${extractLegalBoilerplateSection(prTemplateContent)}
280+
281+
This is required to ensure proper intellectual property rights for your contributions.
282+
`.trim());
283+
return;
284+
}
285+
286+
console.log('::debug:: Legal boilerplate found in PR description ✓');
287+
}
288+
289+
/// Extract the legal boilerplate section from the PR template
290+
function extractLegalBoilerplateSection(templateContent) {
291+
// Find the legal boilerplate section and extract it
292+
const lines = templateContent.split('\n');
293+
let inLegalSection = false;
294+
let legalSection = [];
295+
296+
for (let i = 0; i < lines.length; i++) {
297+
const line = lines[i];
298+
299+
// Check if this line is the legal boilerplate header
300+
if (/^#{1,6}\s+Legal\s+Boilerplate/i.test(line)) {
301+
inLegalSection = true;
302+
legalSection.push(line);
303+
continue;
304+
}
305+
306+
// If we're in the legal section
307+
if (inLegalSection) {
308+
// Check if we've reached another header (end of legal section)
309+
if (/^#{1,6}\s+/.test(line)) {
310+
break;
311+
}
312+
legalSection.push(line);
313+
}
314+
}
315+
316+
return legalSection.join('\n').trim();
317+
}
318+
189319
async function checkFromExternalChecks() {
190320
// Get the external dangerfile path from environment variable (passed via workflow input)
191321
// Priority: EXTRA_DANGERFILE (absolute path) -> EXTRA_DANGERFILE_INPUT (relative path)
@@ -231,6 +361,7 @@ async function checkAll() {
231361
await checkDocs();
232362
await checkChangelog();
233363
await checkActionsArePinned();
364+
await checkLegalBoilerplate();
234365
await checkFromExternalChecks();
235366
}
236367

0 commit comments

Comments
 (0)