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