Skip to content

Commit abbdcf8

Browse files
authored
Update CLA checker workflow (#44)
1 parent 4d018dc commit abbdcf8

1 file changed

Lines changed: 143 additions & 27 deletions

File tree

.github/workflows/cla-check.yaml

Lines changed: 143 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
name: CLA
1+
# ------------------------------------------------------------------------------
2+
# (c) Crown copyright Met Office. All rights reserved.
3+
# The file LICENCE, distributed with this code, contains details of the terms
4+
# under which the code may be used.
5+
# ------------------------------------------------------------------------------
6+
name: CLA Checker
27
on:
38
workflow_call:
49
inputs:
@@ -18,12 +23,12 @@ jobs:
1823
steps:
1924
# --- Step 1: Check Base Branch ---
2025
- name: Checkout base branch and check for contributor status
21-
uses: actions/checkout@v5
26+
uses: actions/checkout@v6
2227
with:
2328
token: ${{ secrets.GITHUB_TOKEN }}
24-
ref: ${{ github.event.pull_request.base.sha }}
29+
ref: ${{ github.ref }}
2530

26-
- name: Determine if contributor exists in base
31+
- name: Determine if contributor exists in base (new)
2732
id: check_contributor_base
2833
run: |
2934
AUTHOR="${{ github.event.pull_request.user.login }}"
@@ -43,28 +48,52 @@ jobs:
4348
4449
# --- Step 2: Check PR Branch ---
4550
- name: Checkout PR branch and check for contributor status
46-
# Only run if contributor wasn't found on the base branch
47-
if: steps.check_contributor_base.outputs.on_base != 'true'
48-
uses: actions/checkout@v5
51+
# Always check PR branch to detect if contributor removed themselves
52+
uses: actions/checkout@v6
4953
with:
5054
token: ${{ secrets.GITHUB_TOKEN }}
5155
ref: ${{ github.event.pull_request.head.sha }}
5256

5357
- name: Determine if contributor exists in PR branch
5458
id: check_contributor_pr
55-
# Only run if contributor wasn't found on the base branch
56-
if: steps.check_contributor_base.outputs.on_base != 'true'
59+
# Always check PR branch to detect if contributor removed themselves
5760
run: |
5861
AUTHOR="${{ github.event.pull_request.user.login }}"
59-
if grep -q "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then
60-
echo "signed=true" >> $GITHUB_OUTPUT
61-
echo "✅ $AUTHOR has updated their CLA signature in CONTRIBUTORS.md file."
62+
if [ -f "CONTRIBUTORS.md" ]; then
63+
if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then
64+
echo "on_pr=true" >> $GITHUB_OUTPUT
65+
echo "✅ $AUTHOR is in the CONTRIBUTORS.md file on PR branch."
66+
else
67+
echo "on_pr=false" >> $GITHUB_OUTPUT
68+
echo "⚠️ $AUTHOR is not in the CONTRIBUTORS.md file on PR branch."
69+
fi
70+
else
71+
echo "on_pr=undefined" >> $GITHUB_OUTPUT
72+
echo "🔴 CONTRIBUTORS.md file does not exist on PR branch."
73+
fi
74+
75+
# -- Check if CONTRIBUTORS.md was modified in this PR
76+
- name: Check if CONTRIBUTORS.md was modified in PR
77+
id: check_contributors_modified
78+
run: |
79+
# Fetch the base branch
80+
git fetch origin ${{ github.event.pull_request.base.ref }}
81+
82+
# Use the base SHA directly for comparison (works with forks)
83+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
84+
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
85+
86+
CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA)
87+
88+
if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then
89+
echo "modified=true" >> $GITHUB_OUTPUT
90+
echo "📝 CONTRIBUTORS.md file was modified in this PR."
6291
else
63-
echo "signed=false" >> $GITHUB_OUTPUT
64-
echo "❌ $AUTHOR has not signed the CLA."
92+
echo "modified=false" >> $GITHUB_OUTPUT
93+
echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR."
6594
fi
6695
67-
# --- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) ---
96+
# -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated)
6897
- name: Manage CLA Status, Labels, and Comments
6998
uses: actions/github-script@v8
7099
# Using 'always()' here so this step runs regardless of previous
@@ -74,44 +103,128 @@ jobs:
74103
github-token: ${{ secrets.GITHUB_TOKEN }}
75104
script: |
76105
const signedOnBase = '${{ steps.check_contributor_base.outputs.on_base }}' === 'true';
77-
// Default signed status is false if the second check wasn't run
78-
const signedOnPr = '${{ steps.check_contributor_pr.outputs.signed }}' === 'true';
79-
const cla_met = signedOnBase || signedOnPr;
106+
const signedOnPr = '${{ steps.check_contributor_pr.outputs.on_pr }}' === 'true';
107+
const contributorsModified = '${{ steps.check_contributors_modified.outputs.modified }}' === 'true';
80108
const issue_number = context.issue.number;
81109
const owner = context.repo.owner;
82110
const repo = context.repo.repo;
83111
const author = context.payload.pull_request.user.login;
84112
113+
// Check if contributor was on base but removed themselves from PR
114+
const removedFromPr = signedOnBase && !signedOnPr && contributorsModified;
115+
// CLA is met if: (1) signed on both base and PR, OR (2) signed on base and didn't modify CONTRIBUTORS.md
116+
const cla_met = (signedOnBase && signedOnPr) || (signedOnBase && !contributorsModified);
117+
85118
// Helper function to create or update a label with a specific color
86119
async function ensureLabel(name, color, description) {
87120
try {
88121
await github.rest.issues.updateLabel({ owner, repo, name, color, description });
89122
console.log(`Updated label: ${name} with color ${color}`);
90123
} catch (error) {
91124
// If update fails (label doesn't exist), create it
92-
await github.rest.issues.createLabel({ owner, repo, name, color, description });
93-
console.log(`Created new label: ${name} with color ${color}`);
125+
try {
126+
await github.rest.issues.createLabel({ owner, repo, name, color, description });
127+
console.log(`Created new label: ${name} with color ${color}`);
128+
} catch (createError) {
129+
console.log(`Error with label ${name}:`, createError.message);
130+
}
94131
}
95132
}
96133
97134
// Define desired colors and descriptions for consistency
98135
const COLOR_SIGNED = '0052cc'; // Blue
99136
const COLOR_REQUIRED = 'b60205'; // Red
100137
101-
console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr})`);
138+
// Helper function to delete old CLA-related comments from this bot
139+
async function deleteOldClaComments() {
140+
try {
141+
const comments = await github.rest.issues.listComments({
142+
owner,
143+
repo,
144+
issue_number
145+
});
146+
147+
// Filter comments from GitHub Actions bot that contain CLA-related content
148+
// GitHub Actions bot username is 'github-actions[bot]'
149+
const botComments = comments.data.filter(comment =>
150+
(comment.user.login === 'github-actions[bot]' || comment.user.type === 'Bot') &&
151+
(comment.body.includes('CLA') ||
152+
comment.body.includes('CONTRIBUTORS') ||
153+
comment.body.includes('Contributor Licence Agreement'))
154+
);
155+
156+
console.log(`Found ${botComments.length} old CLA comment(s) to delete`);
157+
158+
// Delete all old CLA comments
159+
for (const comment of botComments) {
160+
await github.rest.issues.deleteComment({
161+
owner,
162+
repo,
163+
comment_id: comment.id
164+
});
165+
console.log(`Deleted old CLA comment #${comment.id} from ${comment.user.login}`);
166+
}
167+
} catch (error) {
168+
console.log('Error deleting old comments:', error.message);
169+
}
170+
}
171+
172+
console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Modified: ${contributorsModified}, Removed: ${removedFromPr})`);
173+
174+
// Handle case where contributor removed themselves from CONTRIBUTORS file
175+
if (removedFromPr) {
176+
await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.');
177+
console.log('⚠️ Contributor was in base branch but removed from PR branch.');
178+
179+
// Ensure labels are correct
180+
await Promise.allSettled([
181+
github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-signed' }),
182+
github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] })
183+
]);
184+
185+
// Delete old CLA comments before posting new one
186+
await deleteOldClaComments();
187+
188+
// Post warning comment
189+
const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have removed yourself from the _CONTRIBUTORS.md_ file in this PR.\n\nPlease ensure your entry remains in the _CONTRIBUTORS.md_ file. If you have already signed the CLA, you should not remove your details from the file.`;
190+
191+
await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody });
192+
193+
// Fail the GitHub Action run
194+
console.error("⚠️ Contributor removed themselves from CONTRIBUTORS file.");
195+
process.exit(1);
196+
}
102197
103198
if (cla_met) {
104199
await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.');
105-
console.log('✅ CLA condition met. Removing required label and adding signed label.');
200+
201+
// Different messages based on scenario
202+
if (signedOnBase && !contributorsModified) {
203+
console.log('✅ CLA already signed on base branch, and CONTRIBUTORS.md not modified in PR.');
204+
} else {
205+
console.log('✅ CLA condition met. Removing required label and adding signed label.');
206+
}
207+
106208
// Use Promise.allSettled for robust label management
107209
await Promise.allSettled([
108210
github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }),
211+
github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] })
109212
]);
110-
if ( signedOnBase === false ) {
111-
await Promise.allSettled([
112-
github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] })
113-
]);
114-
}
213+
214+
// Delete old CLA comments since CLA is satisfied
215+
await deleteOldClaComments();
216+
217+
} else if (!signedOnBase && signedOnPr) {
218+
// New contributor signing CLA for the first time
219+
await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.');
220+
console.log('✅ New contributor has signed the CLA in PR branch.');
221+
await Promise.allSettled([
222+
github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }),
223+
github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] })
224+
]);
225+
226+
// Delete old CLA comments since CLA is now signed
227+
await deleteOldClaComments();
115228
116229
} else {
117230
await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.');
@@ -123,6 +236,9 @@ jobs:
123236
github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] })
124237
]);
125238
239+
// Delete old CLA comments before posting new one
240+
await deleteOldClaComments();
241+
126242
// Post CLA comment
127243
const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/Momentum/blob/main/CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, Real Name, Affiliation, and Date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`;
128244

0 commit comments

Comments
 (0)