1name: Checklist 2 3# Produce a list of things that need to be changed 4# for the submission to align with CONTRIBUTING.md 5 6on: 7 pull_request_target: 8 types: [ opened, reopened, edited, synchronize ] 9 10permissions: 11 pull-requests: write 12 13jobs: 14 checklist: 15 name: commit 16 if: github.repository == 'freebsd/freebsd-src' 17 runs-on: ubuntu-latest 18 steps: 19 - uses: actions/github-script@v7 20 with: 21 # An asynchronous javascript function 22 script: | 23 /* 24 * Github's API returns the results in pages of 30, so 25 * pass the function we want, along with it's arguments, 26 * to paginate() which will handle gathering all the results. 27 */ 28 const comments = await github.paginate(github.rest.issues.listComments, { 29 owner: context.repo.owner, 30 repo: context.repo.repo, 31 issue_number: context.issue.number 32 }); 33 34 const commits = await github.paginate(github.rest.pulls.listCommits, { 35 owner: context.repo.owner, 36 repo: context.repo.repo, 37 pull_number: context.issue.number 38 }); 39 40 /* Get owners */ 41 42 let owners = []; 43 const { data: ownerData } = await github.rest.repos.getContent({ 44 owner: context.repo.owner, 45 repo: context.repo.repo, 46 path: '.github/CODEOWNERS', 47 ref: context.payload.pull_request.base.ref // Or a specific branch 48 }); 49 const oc = Buffer.from(ownerData.content, 'base64').toString(); 50 owners = oc.split(/\r?\n/) 51 .map(line => line.trim()) 52 // Filter out comments and empty lines 53 .filter(line => line && !line.startsWith('#')) 54 .map(line => { 55 // Split by the first block of whitespace to separate path and message 56 const [path, ...ownerParts] = line.substring(1).split(/\s+/); 57 return { path, owner: ownerParts.join(' ') }; 58 }); 59 60 /* Get rules -- maybe refactor to a function for ownerPath too */ 61 let rules = []; 62 const { data: rulesData } = await github.rest.repos.getContent({ 63 owner: context.repo.owner, 64 repo: context.repo.repo, 65 path: '.github/path-rules.txt', 66 ref: context.payload.pull_request.base.ref // Or a specific branch 67 }); 68 const rc = Buffer.from(rulesData.content, 'base64').toString(); 69 rules = rc.split(/\r?\n/) 70 .map(line => line.trim()) 71 // Filter out comments and empty lines 72 .filter(line => line && !line.startsWith('#')) 73 .map(line => { 74 // Split by the first block of whitespace to separate path and message 75 const [path, ...messageParts] = line.split(/\s+/); 76 return { path, message: messageParts.join(' ') }; 77 }); 78 79 let checklist = {}; 80 let checklist_len = 0; 81 let comment_id = -1; 82 83 const addToChecklist = (msg, sha) => { 84 if (!checklist[msg]) { 85 checklist[msg] = []; 86 checklist_len++; 87 } 88 checklist[msg].push(sha); 89 } 90 91 for (const commit of commits) { 92 const sob_lines = commit.commit.message.match(/^[^\S\r\n]*signed-off-by:.*/gim); 93 94 if (sob_lines == null && !commit.commit.author.email.toLowerCase().endsWith("freebsd.org")) 95 addToChecklist("Missing Signed-off-by lines", commit.sha); 96 else if (sob_lines != null) { 97 let author_signed = false; 98 for (const line of sob_lines) { 99 if (!line.includes("Signed-off-by: ")) 100 /* Only display the part we care about. */ 101 addToChecklist("Expected `Signed-off-by: `, got `" + line.match(/^[^\S\r\n]*signed-off-by:./i) + "`", commit.sha); 102 if (line.includes(commit.commit.author.email)) 103 author_signed = true; 104 } 105 106 if (!author_signed) 107 console.log("::warning title=Missing-Author-Signature::Missing Signed-off-by from author"); 108 } 109 110 if (commit.commit.author.email.toLowerCase().includes("noreply")) 111 addToChecklist("Real email address is needed", commit.sha); 112 } 113 114 /* Check for different paths that have issues and/or owners */ 115 const { data: files } = await github.rest.pulls.listFiles({ 116 owner: context.repo.owner, 117 repo: context.repo.repo, 118 pull_number: context.payload.pull_request.number, 119 }); 120 121 let infolist = {}; 122 let infolist_len = 0; 123 const addToInfolist = (msg) => { 124 if (!infolist[msg]) { 125 infolist[msg] = []; 126 infolist_len++; 127 } 128 } 129 130 /* Give advice based on what's in the commit */ 131 for (const file of files) { 132 for (const owner of owners) { 133 if (file.filename.startsWith(owner.path)) { 134 addToInfolist("> [!IMPORTANT]\n> " + owner.owner + " wants to review changes to " + owner.path + "\n"); 135 } 136 } 137 for (const rule of rules) { 138 // Consider regexp in the future maybe? 139 if (file.filename.startsWith(rule.path)) { 140 if (rule.message.startsWith(":caution: ")) { 141 addToInfolist("> [!CAUTION]\n> " + rule.path + ": " + rule.message.substring(10) + "\n"); 142 } else if (rule.message.startsWith(":warning: ")) { 143 addToInfolist("> [!WARNING]\n> " + rule.path + ": " + rule.message.substring(10) + "\n"); 144 } else { 145 addToInfolist("> [!IMPORTANT]\n> " + rule.path + ": " + rule.message + "\n"); 146 } 147 } 148 } 149 } 150 151 /* Check if we've commented before. */ 152 for (const comment of comments) { 153 if (comment.user.login == "github-actions[bot]") { 154 comment_id = comment.id; 155 break; 156 } 157 } 158 159 const msg_prefix = "Thank you for taking the time to contribute to FreeBSD!\n\n"; 160 if (checklist_len != 0 || infolist_len != 0) { 161 let msg = msg_prefix; 162 let comment_func = comment_id == -1 ? github.rest.issues.createComment : github.rest.issues.updateComment; 163 if (checklist_len != 0) { 164 msg += 165 "There " + (checklist_len > 1 ? "are a few issues that need " : "is an issue that needs ") + 166 "to be resolved:\n"; 167 168 /* Loop for each key in "checklist". */ 169 for (const c in checklist) 170 msg += "- " + c + " (" + checklist[c].join(", ") + ")\n"; 171 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" 172 } else { 173 let msg = "No Issues found.\n\n"; 174 } 175 if (infolist_len != 0) { 176 msg += "Some of files have special handling:\n" 177 for (const i in infolist) 178 msg += i + "\n"; 179 msg += "\n\n"; 180 } 181 comment_func({ 182 owner: context.repo.owner, 183 repo: context.repo.repo, 184 body: msg, 185 ...(comment_id == -1 ? {issue_number: context.issue.number} : {comment_id: comment_id}) 186 }); 187 } else if (comment_id != -1) { 188 github.rest.issues.updateComment({ 189 owner: context.repo.owner, 190 repo: context.repo.repo, 191 comment_id: comment_id, 192 body: msg_prefix + "All issues resolved." 193 }); 194 } 195