chore: dco advisor (#1795)
Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>
This commit is contained in:
parent
7bae3b6c06
commit
c2ef69718a
2
.github/dco.yml
vendored
Normal file
2
.github/dco.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
allowRemediationCommits:
|
||||||
|
individual: true
|
192
.github/workflows/dco-advisor.yml
vendored
Normal file
192
.github/workflows/dco-advisor.yml
vendored
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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 = '<!-- dco-advice-bot -->';
|
||||||
|
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',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'<details>',
|
||||||
|
'<summary>🔧 Advanced: Sign off each commit directly</summary>',
|
||||||
|
'',
|
||||||
|
'**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',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'</details>',
|
||||||
|
'',
|
||||||
|
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("<!-- dco-advice-bot -->")
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user