Skip to content

Commit 07ec9e7

Browse files
committed
CI: Add a check for verified commits and an automation that leaves a comment if commits aren't verified.
1 parent fa59df8 commit 07ec9e7

File tree

2 files changed

+222
-0
lines changed

2 files changed

+222
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
name: PR Signature Comment
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, synchronize]
6+
7+
jobs:
8+
check-and-comment:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
pull-requests: write
12+
13+
steps:
14+
- name: Check for unsigned commits and comment
15+
uses: actions/github-script@v7
16+
with:
17+
script: |
18+
// Get all commits in this PR
19+
const commits = await github.rest.pulls.listCommits({
20+
owner: context.repo.owner,
21+
repo: context.repo.repo,
22+
pull_number: context.issue.number
23+
});
24+
25+
// Check for unsigned commits
26+
const unsignedCommits = commits.data.filter(commit =>
27+
!commit.commit.verification || !commit.commit.verification.verified
28+
);
29+
30+
if (unsignedCommits.length === 0) {
31+
console.log('All commits are signed. No comment needed.');
32+
return;
33+
}
34+
35+
console.log(`Found ${unsignedCommits.length} unsigned commit(s).`);
36+
37+
// Check if we already left a comment about signatures
38+
const comments = await github.rest.issues.listComments({
39+
owner: context.repo.owner,
40+
repo: context.repo.repo,
41+
issue_number: context.issue.number
42+
});
43+
44+
const signatureCommentMarker = '<!-- signature-reminder-comment -->';
45+
const existingComment = comments.data.find(comment =>
46+
comment.body.includes(signatureCommentMarker)
47+
);
48+
49+
const unsignedList = unsignedCommits.map(c =>
50+
`- \`${c.sha.substring(0, 7)}\` ${c.commit.message.split('\n')[0]}`
51+
).join('\n');
52+
53+
const commentBody = `${signatureCommentMarker}
54+
⚠️ **Unsigned Commits Detected**
55+
56+
This pull request contains ${unsignedCommits.length} unsigned commit(s):
57+
58+
${unsignedList}
59+
60+
All commits must be signed before this PR can be merged. Please sign your commits by following these steps:
61+
62+
### Option 1: SSH Key Signing (Recommended)
63+
64+
1. **Add your SSH key to GitHub** (if not already done):
65+
- Go to [GitHub SSH Keys Settings](https://github.com/settings/keys)
66+
- Click "New SSH Key" and select "Signing Key" as the key type
67+
68+
2. **Configure Git to use SSH signing**:
69+
\`\`\`bash
70+
git config --global gpg.format ssh
71+
git config --global user.signingkey ~/.ssh/id_ed25519.pub # or your key path
72+
git config --global commit.gpgsign true
73+
\`\`\`
74+
75+
3. **Re-sign your commits**:
76+
\`\`\`bash
77+
git rebase -i HEAD~${unsignedCommits.length} --exec "git commit --amend --no-edit -S"
78+
git push --force
79+
\`\`\`
80+
81+
### Option 2: GPG Key Signing
82+
83+
1. **Generate a GPG key**: [GitHub Docs](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key)
84+
85+
2. **Add the GPG key to GitHub**: [GitHub Docs](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account)
86+
87+
3. **Configure Git to sign commits**:
88+
\`\`\`bash
89+
git config --global user.signingkey YOUR_GPG_KEY_ID
90+
git config --global commit.gpgsign true
91+
\`\`\`
92+
93+
4. **Re-sign your commits** (same as above):
94+
\`\`\`bash
95+
git rebase -i HEAD~${unsignedCommits.length} --exec "git commit --amend --no-edit -S"
96+
git push --force
97+
\`\`\`
98+
99+
For more details, see [GitHub's commit signature verification docs](https://docs.github.com/en/authentication/managing-commit-signature-verification).
100+
101+
---
102+
*This comment will be updated when you push new commits.*`;
103+
104+
if (existingComment) {
105+
// Update existing comment
106+
await github.rest.issues.updateComment({
107+
owner: context.repo.owner,
108+
repo: context.repo.repo,
109+
comment_id: existingComment.id,
110+
body: commentBody
111+
});
112+
console.log('Updated existing signature reminder comment.');
113+
} else {
114+
// Create new comment
115+
await github.rest.issues.createComment({
116+
owner: context.repo.owner,
117+
repo: context.repo.repo,
118+
issue_number: context.issue.number,
119+
body: commentBody
120+
});
121+
console.log('Created new signature reminder comment.');
122+
}
123+
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Test Commit Signatures
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
# pull_request is not supported for this workflow due to self-hosted runners
8+
# see the "Reviewing PRs from forks" section in CONTRIBUTING.md for more details
9+
10+
jobs:
11+
test-signatures:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0 # Need full history to compare with main
19+
20+
- name: Check commit signatures
21+
uses: actions/github-script@v7
22+
with:
23+
script: |
24+
const { execSync } = require('child_process');
25+
26+
// Get current branch name
27+
const currentBranch = process.env.GITHUB_REF_NAME;
28+
console.log(`Current branch: ${currentBranch}`);
29+
30+
// Skip signature check on main branch to avoid failing on historical unsigned commits
31+
if (currentBranch === 'main') {
32+
console.log('On main branch - skipping signature check for historical commits.');
33+
console.log('Signature verification only runs on PR branches.');
34+
return;
35+
}
36+
37+
// Get commits that are on this branch but not on origin/main
38+
let commitShas;
39+
try {
40+
const output = execSync('git rev-list origin/main..HEAD', { encoding: 'utf-8' });
41+
commitShas = output.trim().split('\n').filter(sha => sha.length > 0);
42+
} catch (error) {
43+
console.log('Could not compare with origin/main, checking all commits on this branch.');
44+
// Fallback: just check the current commit
45+
commitShas = [process.env.GITHUB_SHA];
46+
}
47+
48+
if (commitShas.length === 0) {
49+
console.log('No new commits to check (branch is up to date with main).');
50+
return;
51+
}
52+
53+
console.log(`Checking ${commitShas.length} commit(s) for signatures...`);
54+
55+
// Check each commit's verification status via GitHub API
56+
const unsignedCommits = [];
57+
58+
for (const sha of commitShas) {
59+
try {
60+
const commit = await github.rest.repos.getCommit({
61+
owner: context.repo.owner,
62+
repo: context.repo.repo,
63+
ref: sha
64+
});
65+
66+
const verification = commit.data.commit.verification;
67+
const shortSha = sha.substring(0, 7);
68+
const message = commit.data.commit.message.split('\n')[0];
69+
70+
if (verification && verification.verified) {
71+
console.log(`✓ ${shortSha} - ${message} (${verification.reason})`);
72+
} else {
73+
const reason = verification ? verification.reason : 'unknown';
74+
console.log(`✗ ${shortSha} - ${message} (${reason})`);
75+
unsignedCommits.push({
76+
sha: shortSha,
77+
fullSha: sha,
78+
message: message,
79+
reason: reason
80+
});
81+
}
82+
} catch (error) {
83+
console.log(`⚠ ${sha.substring(0, 7)} - Could not verify (${error.message})`);
84+
// Don't fail on API errors for individual commits
85+
}
86+
}
87+
88+
if (unsignedCommits.length > 0) {
89+
console.log('\n❌ Found unsigned commits:');
90+
for (const commit of unsignedCommits) {
91+
console.log(` ${commit.sha}: ${commit.message} (reason: ${commit.reason})`);
92+
}
93+
console.log('\nAll commits must be signed. See the PR comments for instructions on how to sign commits.');
94+
console.log('You can re-sign commits with: git rebase -i HEAD~N --exec "git commit --amend --no-edit -S"');
95+
core.setFailed(`${unsignedCommits.length} unsigned commit(s) found. All commits must be signed.`);
96+
} else {
97+
console.log(`\n✅ All ${commitShas.length} commit(s) are properly signed.`);
98+
}
99+

0 commit comments

Comments
 (0)