name: DCO Advisor Bot on: pull_request_target: types: [opened, reopened, synchronize] permissions: pull-requests: write issues: write jobs: dco_advisor: runs-on: ubuntu-latest steps: - name: Handle DCO check result uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const pr = context.payload.pull_request || context.payload.check_run?.pull_requests?.[0]; if (!pr) return; const prNumber = pr.number; const baseRef = pr.base.ref; const headSha = context.payload.check_run?.head_sha || pr.head?.sha; const username = pr.user.login; console.log("HEAD SHA:", headSha); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Poll until DCO check has a conclusion (max 6 attempts, 30s) let dcoCheck = null; for (let attempt = 0; attempt < 6; attempt++) { const { data: checks } = await github.rest.checks.listForRef({ owner: context.repo.owner, repo: context.repo.repo, ref: headSha }); console.log("All check runs:"); checks.check_runs.forEach(run => { console.log(`- ${run.name} (${run.status}/${run.conclusion}) @ ${run.head_sha}`); }); dcoCheck = checks.check_runs.find(run => run.name.toLowerCase().includes("dco") && !run.name.toLowerCase().includes("dco_advisor") && run.head_sha === headSha ); if (dcoCheck?.conclusion) break; console.log(`Waiting for DCO check... (${attempt + 1})`); await sleep(5000); // wait 5 seconds } if (!dcoCheck || !dcoCheck.conclusion) { console.log("DCO check did not complete in time."); return; } const isFailure = ["failure", "action_required"].includes(dcoCheck.conclusion); console.log(`DCO check conclusion for ${headSha}: ${dcoCheck.conclusion} (treated as ${isFailure ? "failure" : "success"})`); // Parse DCO output for commit SHAs and author let badCommits = []; let authorName = ""; let authorEmail = ""; let moreInfo = `More info: [DCO check report](${dcoCheck?.html_url})`; if (isFailure) { const { data: commits } = await github.rest.pulls.listCommits({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, }); for (const commit of commits) { const commitMessage = commit.commit.message; const signoffMatch = commitMessage.match(/^Signed-off-by:\s+.+<.+>$/m); if (!signoffMatch) { console.log(`Bad commit found ${commit.sha}`) badCommits.push({ sha: commit.sha, authorName: commit.commit.author.name, authorEmail: commit.commit.author.email, }); } } } // If multiple authors are present, you could adapt the message accordingly // For now, we'll just use the first one if (badCommits.length > 0) { authorName = badCommits[0].authorName; authorEmail = badCommits[0].authorEmail; } // Generate remediation commit message if needed let remediationSnippet = ""; if (badCommits.length && authorEmail) { remediationSnippet = `git commit --allow-empty -s -m "DCO Remediation Commit for ${authorName} <${authorEmail}>\n\n` + badCommits.map(c => `I, ${c.authorName} <${c.authorEmail}>, hereby add my Signed-off-by to this commit: ${c.sha}`).join('\n') + `"`; } else { remediationSnippet = "# Unable to auto-generate remediation message. Please check the DCO check details."; } // Build comment const commentHeader = ''; let body = ""; if (isFailure) { body = [ commentHeader, '❌ **DCO Check Failed**', '', `Hi @${username}, your pull request has failed the Developer Certificate of Origin (DCO) check.`, '', 'This repository supports **remediation commits**, so you can fix this without rewriting history — but you must follow the required message format.', '', '---', '', '### 🛠 Quick Fix: Add a remediation commit', 'Run this command:', '', '```bash', remediationSnippet, 'git push', '```', '', '---', '', '
', '🔧 Advanced: Sign off each commit directly', '', '**For the latest commit:**', '```bash', 'git commit --amend --signoff', 'git push --force-with-lease', '```', '', '**For multiple commits:**', '```bash', `git rebase --signoff origin/${baseRef}`, 'git push --force-with-lease', '```', '', '
', '', moreInfo ].join('\n'); } else { body = [ commentHeader, '✅ **DCO Check Passed**', '', `Thanks @${username}, all your commits are properly signed off. 🎉` ].join('\n'); } // Get existing comments on the PR const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); // Look for a previous bot comment const existingComment = comments.find(c => c.body.includes("") ); if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingComment.id, body: body }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: body }); }