Skip to content

Commit 01e87b8

Browse files
committed
Signed commit gate
1 parent c945ab9 commit 01e87b8

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Check PR Commit Signatures
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
pr_number:
7+
description: 'The Pull Request number to inspect'
8+
required: true
9+
type: integer
10+
11+
permissions: {} # Restrict token access to the minimum required for this workflow
12+
13+
jobs:
14+
verify:
15+
runs-on: ubuntu-slim
16+
permissions:
17+
contents: read # Restrict token access strictly to reading repository contents
18+
pull-requests: write # Restrict token access strictly to writing PR comments
19+
20+
steps:
21+
- name: Check signatures and manage PR comments
22+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
23+
with:
24+
script: |
25+
// Enforce strictly typed parameter input to prevent code injection
26+
const prNumber = Number('${{ inputs.pr_number }}');
27+
if (isNaN(prNumber)) {
28+
core.setFailed("Invalid PR number input context.");
29+
return;
30+
}
31+
32+
const commentIdentifier = "<!-- id: growss-signature-gate-bot-comment -->";
33+
34+
// 1. Fetch verification status of all PR commits using GraphQL API
35+
const query = `
36+
query($owner: String!, $repo: String!, $pr: Int!) {
37+
repository(owner: $owner, name: $repo) {
38+
pullRequest(number: $pr) {
39+
commits(first: 100) {
40+
nodes {
41+
commit {
42+
oid
43+
signature {
44+
isValid
45+
}
46+
}
47+
}
48+
}
49+
}
50+
}
51+
}
52+
`;
53+
54+
const variables = {
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
pr: prNumber
58+
};
59+
60+
const result = await github.graphql(query, variables);
61+
const commits = result.repository.pullRequest.commits.nodes;
62+
63+
// 2. Identify unsigned or validation-failing commits
64+
const unsignedCommits = commits.filter(c => !c.commit.signature || !c.commit.signature.isValid);
65+
66+
// 3. Scan for an existing bot comment to update or delete
67+
const comments = await github.rest.issues.listComments({
68+
owner: context.repo.owner,
69+
repo: context.repo.repo,
70+
issue_number: prNumber,
71+
});
72+
73+
const existingComment = comments.data.find(c => c.body.includes(commentIdentifier));
74+
75+
// 4. Handle logic based on validation state
76+
if (unsignedCommits.length > 0) {
77+
const commitList = unsignedCommits.map(c => `- \`${c.commit.oid.substring(0, 7)}\``).join('\n');
78+
const commentBody = `${commentIdentifier}\n.⚠️ **Unsigned Commits Detected**\n\nThe following commits in this Pull Request are missing a verified cryptographic signature:\n\n${commitList}\n\nPlease sign your commits to comply with <a href="https://metoffice.github.io/simulation-systems/WorkingPractices/gh_authorisation.html#verified-commits">project guidelines</a>.`;
79+
80+
if (existingComment) {
81+
// Update the old comment with the refreshed list of unsigned commits
82+
await github.rest.issues.updateComment({
83+
owner: context.repo.owner,
84+
repo: context.repo.repo,
85+
comment_id: existingComment.id,
86+
body: commentBody
87+
});
88+
} else {
89+
// Post a new warning comment
90+
await github.rest.issues.createComment({
91+
owner: context.repo.owner,
92+
repo: context.repo.repo,
93+
issue_number: prNumber,
94+
body: commentBody
95+
});
96+
}
97+
98+
// Force the CI status check to fail outright
99+
core.setFailed("❌ PR block: One or more commits do not have a verified signature.");
100+
101+
} else {
102+
// All commits are signed! Clear old warning comment if it exists
103+
if (existingComment) {
104+
await github.rest.issues.deleteComment({
105+
owner: context.repo.owner,
106+
repo: context.repo.repo,
107+
comment_id: existingComment.id
108+
});
109+
console.log("::notice::Clean state achieved: Deleted the old signature warning comment.");
110+
}
111+
console.log("::success::All commits are properly signed.");
112+
}

0 commit comments

Comments
 (0)