Skip to content

Commit 22d4456

Browse files
authored
chore: dco advisor (#17)
Signed-off-by: Michele Dolfi <[email protected]>
1 parent 51524b4 commit 22d4456

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

.github/dco.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
allowRemediationCommits:
2+
individual: true

.github/workflows/dco-advisor.yml

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
name: DCO Advisor Bot
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, synchronize]
6+
7+
permissions:
8+
pull-requests: write
9+
issues: write
10+
11+
jobs:
12+
dco_advisor:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Handle DCO check result
16+
uses: actions/github-script@v7
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
script: |
20+
const pr = context.payload.pull_request || context.payload.check_run?.pull_requests?.[0];
21+
if (!pr) return;
22+
23+
const prNumber = pr.number;
24+
const baseRef = pr.base.ref;
25+
const headSha =
26+
context.payload.check_run?.head_sha ||
27+
pr.head?.sha;
28+
const username = pr.user.login;
29+
30+
console.log("HEAD SHA:", headSha);
31+
32+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
33+
34+
// Poll until DCO check has a conclusion (max 6 attempts, 30s)
35+
let dcoCheck = null;
36+
for (let attempt = 0; attempt < 6; attempt++) {
37+
const { data: checks } = await github.rest.checks.listForRef({
38+
owner: context.repo.owner,
39+
repo: context.repo.repo,
40+
ref: headSha
41+
});
42+
43+
44+
console.log("All check runs:");
45+
checks.check_runs.forEach(run => {
46+
console.log(`- ${run.name} (${run.status}/${run.conclusion}) @ ${run.head_sha}`);
47+
});
48+
49+
dcoCheck = checks.check_runs.find(run =>
50+
run.name.toLowerCase().includes("dco") &&
51+
!run.name.toLowerCase().includes("dco_advisor") &&
52+
run.head_sha === headSha
53+
);
54+
55+
56+
if (dcoCheck?.conclusion) break;
57+
console.log(`Waiting for DCO check... (${attempt + 1})`);
58+
await sleep(5000); // wait 5 seconds
59+
}
60+
61+
if (!dcoCheck || !dcoCheck.conclusion) {
62+
console.log("DCO check did not complete in time.");
63+
return;
64+
}
65+
66+
const isFailure = ["failure", "action_required"].includes(dcoCheck.conclusion);
67+
console.log(`DCO check conclusion for ${headSha}: ${dcoCheck.conclusion} (treated as ${isFailure ? "failure" : "success"})`);
68+
69+
// Parse DCO output for commit SHAs and author
70+
let badCommits = [];
71+
let authorName = "";
72+
let authorEmail = "";
73+
let moreInfo = `More info: [DCO check report](${dcoCheck?.html_url})`;
74+
75+
if (isFailure) {
76+
const { data: commits } = await github.rest.pulls.listCommits({
77+
owner: context.repo.owner,
78+
repo: context.repo.repo,
79+
pull_number: prNumber,
80+
});
81+
82+
for (const commit of commits) {
83+
const commitMessage = commit.commit.message;
84+
const signoffMatch = commitMessage.match(/^Signed-off-by:\s+.+<.+>$/m);
85+
if (!signoffMatch) {
86+
console.log(`Bad commit found ${commit.sha}`)
87+
badCommits.push({
88+
sha: commit.sha,
89+
authorName: commit.commit.author.name,
90+
authorEmail: commit.commit.author.email,
91+
});
92+
}
93+
}
94+
}
95+
96+
// If multiple authors are present, you could adapt the message accordingly
97+
// For now, we'll just use the first one
98+
if (badCommits.length > 0) {
99+
authorName = badCommits[0].authorName;
100+
authorEmail = badCommits[0].authorEmail;
101+
}
102+
103+
// Generate remediation commit message if needed
104+
let remediationSnippet = "";
105+
if (badCommits.length && authorEmail) {
106+
remediationSnippet = `git commit --allow-empty -s -m "DCO Remediation Commit for ${authorName} <${authorEmail}>\n\n` +
107+
badCommits.map(c => `I, ${c.authorName} <${c.authorEmail}>, hereby add my Signed-off-by to this commit: ${c.sha}`).join('\n') +
108+
`"`;
109+
} else {
110+
remediationSnippet = "# Unable to auto-generate remediation message. Please check the DCO check details.";
111+
}
112+
113+
// Build comment
114+
const commentHeader = '<!-- dco-advice-bot -->';
115+
let body = "";
116+
117+
if (isFailure) {
118+
body = [
119+
commentHeader,
120+
'❌ **DCO Check Failed**',
121+
'',
122+
`Hi @${username}, your pull request has failed the Developer Certificate of Origin (DCO) check.`,
123+
'',
124+
'This repository supports **remediation commits**, so you can fix this without rewriting history — but you must follow the required message format.',
125+
'',
126+
'---',
127+
'',
128+
'### 🛠 Quick Fix: Add a remediation commit',
129+
'Run this command:',
130+
'',
131+
'```bash',
132+
remediationSnippet,
133+
'git push',
134+
'```',
135+
'',
136+
'---',
137+
'',
138+
'<details>',
139+
'<summary>🔧 Advanced: Sign off each commit directly</summary>',
140+
'',
141+
'**For the latest commit:**',
142+
'```bash',
143+
'git commit --amend --signoff',
144+
'git push --force-with-lease',
145+
'```',
146+
'',
147+
'**For multiple commits:**',
148+
'```bash',
149+
`git rebase --signoff origin/${baseRef}`,
150+
'git push --force-with-lease',
151+
'```',
152+
'',
153+
'</details>',
154+
'',
155+
moreInfo
156+
].join('\n');
157+
} else {
158+
body = [
159+
commentHeader,
160+
'✅ **DCO Check Passed**',
161+
'',
162+
`Thanks @${username}, all your commits are properly signed off. 🎉`
163+
].join('\n');
164+
}
165+
166+
// Get existing comments on the PR
167+
const { data: comments } = await github.rest.issues.listComments({
168+
owner: context.repo.owner,
169+
repo: context.repo.repo,
170+
issue_number: prNumber
171+
});
172+
173+
// Look for a previous bot comment
174+
const existingComment = comments.find(c =>
175+
c.body.includes("<!-- dco-advice-bot -->")
176+
);
177+
178+
if (existingComment) {
179+
await github.rest.issues.updateComment({
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
comment_id: existingComment.id,
183+
body: body
184+
});
185+
} else {
186+
await github.rest.issues.createComment({
187+
owner: context.repo.owner,
188+
repo: context.repo.repo,
189+
issue_number: prNumber,
190+
body: body
191+
});
192+
}

0 commit comments

Comments
 (0)