name: Checklist # Produce a list of things that need to be changed # for the submission to align with CONTRIBUTING.md on: pull_request_target: types: [ opened, reopened, edited, synchronize ] permissions: pull-requests: write jobs: checklist: name: commit if: github.repository == 'freebsd/freebsd-src' runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: # An asynchronous javascript function script: | /* * Github's API returns the results in pages of 30, so * pass the function we want, along with it's arguments, * to paginate() which will handle gathering all the results. */ const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); const commits = await github.paginate(github.rest.pulls.listCommits, { owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number }); /* Get owners */ let owners = []; const { data: ownerData } = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path: '.github/CODEOWNERS', ref: context.payload.pull_request.base.ref // Or a specific branch }); const oc = Buffer.from(ownerData.content, 'base64').toString(); owners = oc.split(/\r?\n/) .map(line => line.trim()) // Filter out comments and empty lines .filter(line => line && !line.startsWith('#')) .map(line => { // Split by the first block of whitespace to separate path and message const [path, ...ownerParts] = line.substring(1).split(/\s+/); return { path, owner: ownerParts.join(' ') }; }); /* Get rules -- maybe refactor to a function for ownerPath too */ let rules = []; const { data: rulesData } = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path: '.github/path-rules.txt', ref: context.payload.pull_request.base.ref // Or a specific branch }); const rc = Buffer.from(rulesData.content, 'base64').toString(); rules = rc.split(/\r?\n/) .map(line => line.trim()) // Filter out comments and empty lines .filter(line => line && !line.startsWith('#')) .map(line => { // Split by the first block of whitespace to separate path and message const [path, ...messageParts] = line.split(/\s+/); return { path, message: messageParts.join(' ') }; }); let checklist = {}; let checklist_len = 0; let comment_id = -1; const addToChecklist = (msg, sha) => { if (!checklist[msg]) { checklist[msg] = []; checklist_len++; } checklist[msg].push(sha); } for (const commit of commits) { const sob_lines = commit.commit.message.match(/^[^\S\r\n]*signed-off-by:.*/gim); if (sob_lines == null && !commit.commit.author.email.toLowerCase().endsWith("freebsd.org")) addToChecklist("Missing Signed-off-by lines", commit.sha); else if (sob_lines != null) { let author_signed = false; for (const line of sob_lines) { if (!line.includes("Signed-off-by: ")) /* Only display the part we care about. */ addToChecklist("Expected `Signed-off-by: `, got `" + line.match(/^[^\S\r\n]*signed-off-by:./i) + "`", commit.sha); if (line.includes(commit.commit.author.email)) author_signed = true; } if (!author_signed) console.log("::warning title=Missing-Author-Signature::Missing Signed-off-by from author"); } if (commit.commit.author.email.toLowerCase().includes("noreply")) addToChecklist("Real email address is needed", commit.sha); } /* Check for different paths that have issues and/or owners */ const { data: files } = await github.rest.pulls.listFiles({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request.number, }); let infolist = {}; let infolist_len = 0; const addToInfolist = (msg) => { if (!infolist[msg]) { infolist[msg] = []; infolist_len++; } } /* Give advice based on what's in the commit */ for (const file of files) { for (const owner of owners) { if (file.filename.startsWith(owner.path)) { addToInfolist("> [!IMPORTANT]\n> " + owner.owner + " wants to review changes to " + owner.path + "\n"); } } for (const rule of rules) { // Consider regexp in the future maybe? if (file.filename.startsWith(rule.path)) { if (rule.message.startsWith(":caution: ")) { addToInfolist("> [!CAUTION]\n> " + rule.path + ": " + rule.message.substring(10) + "\n"); } else if (rule.message.startsWith(":warning: ")) { addToInfolist("> [!WARNING]\n> " + rule.path + ": " + rule.message.substring(10) + "\n"); } else { addToInfolist("> [!IMPORTANT]\n> " + rule.path + ": " + rule.message + "\n"); } } } } /* Check if we've commented before. */ for (const comment of comments) { if (comment.user.login == "github-actions[bot]") { comment_id = comment.id; break; } } const msg_prefix = "Thank you for taking the time to contribute to FreeBSD!\n\n"; if (checklist_len != 0 || infolist_len != 0) { let msg = msg_prefix; let comment_func = comment_id == -1 ? github.rest.issues.createComment : github.rest.issues.updateComment; if (checklist_len != 0) { msg += "There " + (checklist_len > 1 ? "are a few issues that need " : "is an issue that needs ") + "to be resolved:\n"; /* Loop for each key in "checklist". */ for (const c in checklist) msg += "- " + c + " (" + checklist[c].join(", ") + ")\n"; msg += "\n> [!NOTE]\n> Please review [CONTRIBUTING.md](https://github.com/freebsd/freebsd-src/blob/main/CONTRIBUTING.md), then update and push your branch again.\n\n" } else { let msg = "No Issues found.\n\n"; } if (infolist_len != 0) { msg += "Some of files have special handling:\n" for (const i in infolist) msg += i + "\n"; msg += "\n\n"; } comment_func({ owner: context.repo.owner, repo: context.repo.repo, body: msg, ...(comment_id == -1 ? {issue_number: context.issue.number} : {comment_id: comment_id}) }); } else if (comment_id != -1) { github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: comment_id, body: msg_prefix + "All issues resolved." }); }