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
27on :
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