Skip to content

Commit ef9ee2d

Browse files
authored
Merge branch 'ClickHouse:main' into date_times_autogen
2 parents d0a24f2 + 07d255e commit ef9ee2d

File tree

5 files changed

+575
-2
lines changed

5 files changed

+575
-2
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
* @ClickHouse/docs
2-
/docs/integrations/ @ClickHouse/integrations @ClickHouse/docs
2+
/docs/integrations/data-ingestion/clickpipes/ @ClickHouse/clickpipes @ClickHouse/docs
3+
/docs/integrations/ @ClickHouse/integrations-ecosystem @ClickHouse/docs
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
name: CLA Approval Handler
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
pr_number:
7+
description: 'PR number to approve CLA for'
8+
required: true
9+
type: string
10+
pull_request_target:
11+
types: [labeled]
12+
issue_comment:
13+
types: [created]
14+
15+
permissions: write-all
16+
17+
jobs:
18+
process-cla-approval:
19+
runs-on: ubuntu-latest
20+
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'cla-signed' || github.event_name == 'issue_comment'
21+
22+
steps:
23+
24+
- name: Generate Token
25+
id: generate-token
26+
continue-on-error: true
27+
uses: actions/create-github-app-token@v1
28+
with:
29+
app-id: "${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }}"
30+
private-key: "${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }}"
31+
32+
- name: Check out code
33+
uses: actions/checkout@v4
34+
with:
35+
fetch-depth: 0
36+
token: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }}
37+
38+
- name: Process CLA approval
39+
id: process-cla-approval
40+
uses: actions/github-script@v7
41+
with:
42+
github-token: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }}
43+
script: |
44+
// Exit early if this is the bot adding labels to prevent double execution
45+
if (context.actor.includes('workflow-authentication-public')) {
46+
console.log(`Skipping execution for bot actor: ${context.actor}`);
47+
return;
48+
}
49+
50+
let prNumber;
51+
let approvedBy;
52+
let prAuthor;
53+
let isCommentApproval = false;
54+
55+
// Handle different event types
56+
if (context.eventName === 'workflow_dispatch') {
57+
prNumber = parseInt('${{ github.event.inputs.pr_number }}');
58+
approvedBy = context.actor;
59+
} else if (context.eventName === 'pull_request_target') {
60+
prNumber = context.payload.pull_request.number;
61+
approvedBy = context.actor;
62+
} else if (context.eventName === 'issue_comment') {
63+
// Only process comments on pull requests
64+
if (!context.payload.issue.pull_request) {
65+
console.log('Comment is not on a pull request, skipping...');
66+
return;
67+
}
68+
69+
prNumber = context.payload.issue.number;
70+
const commentBody = context.payload.comment.body;
71+
const commenter = context.payload.comment.user.login;
72+
73+
// Check if this is a CLA agreement comment
74+
const isClaAgreement = commentBody.includes('I agree to the Trademark License Addendum') &&
75+
commentBody.includes('CLA-SIGNATURE:');
76+
77+
if (!isClaAgreement) {
78+
console.log('Comment is not a CLA agreement, skipping...');
79+
return;
80+
}
81+
82+
// Extract the signature from the comment
83+
const signatureMatch = commentBody.match(/CLA-SIGNATURE:\s*(\S+)/);
84+
if (!signatureMatch) {
85+
console.log('CLA signature format is invalid');
86+
return;
87+
}
88+
89+
const signatureUser = signatureMatch[1];
90+
91+
// Get PR details to verify the commenter is the PR author
92+
const { data: pr } = await github.rest.pulls.get({
93+
owner: context.repo.owner,
94+
repo: context.repo.repo,
95+
pull_number: prNumber
96+
});
97+
98+
// If someone other than PR author is trying to sign, silently ignore
99+
if (commenter !== pr.user.login) {
100+
console.log(`Comment with CLA text from ${commenter} (not PR author ${pr.user.login}), ignoring silently`);
101+
return;
102+
}
103+
104+
// If PR author is signing but signature doesn't match their username, silently ignore
105+
if (signatureUser !== commenter) {
106+
console.log(`PR author ${commenter} used incorrect signature '${signatureUser}', ignoring silently`);
107+
return;
108+
}
109+
110+
// This is a valid CLA agreement comment from the PR author
111+
approvedBy = commenter; // The PR author approved themselves
112+
prAuthor = commenter;
113+
isCommentApproval = true;
114+
console.log(`Valid CLA agreement from PR author: ${commenter}`);
115+
116+
} else {
117+
console.log('Unknown event type, skipping...');
118+
return;
119+
}
120+
121+
// Get PR details if not already retrieved
122+
if (!prAuthor) {
123+
const { data: pr } = await github.rest.pulls.get({
124+
owner: context.repo.owner,
125+
repo: context.repo.repo,
126+
pull_number: prNumber
127+
});
128+
prAuthor = pr.user.login;
129+
}
130+
131+
// For non-comment approvals, check if the person has the right permissions
132+
console.log(`isCommentApproval: ${isCommentApproval}, context.eventName: ${context.eventName}, context.actor: ${context.actor}`);
133+
if (!isCommentApproval) {
134+
try {
135+
const { data: collaboration } = await github.rest.repos.getCollaboratorPermissionLevel({
136+
owner: context.repo.owner,
137+
repo: context.repo.repo,
138+
username: context.actor
139+
});
140+
141+
// Only admin, maintain, or write permissions can manually approve CLA
142+
const isAuthorized = ['admin', 'maintain', 'write'].includes(collaboration.permission);
143+
144+
if (!isAuthorized) {
145+
// If this was a label event, remove the label
146+
if (context.eventName !== 'workflow_dispatch') {
147+
await github.rest.issues.removeLabel({
148+
owner: context.repo.owner,
149+
repo: context.repo.repo,
150+
issue_number: prNumber,
151+
name: 'cla-signed'
152+
});
153+
}
154+
155+
// Add a comment explaining why the action was blocked
156+
await github.rest.issues.createComment({
157+
owner: context.repo.owner,
158+
repo: context.repo.repo,
159+
issue_number: prNumber,
160+
body: `@${context.actor} Only repository maintainers can manually approve CLAs. ${context.eventName !== 'workflow_dispatch' ? 'The label has been removed.' : ''}`
161+
});
162+
163+
return;
164+
}
165+
} catch (error) {
166+
console.error('Error checking permissions:', error);
167+
return;
168+
}
169+
}
170+
171+
// Check if PR has cla-required label
172+
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
173+
owner: context.repo.owner,
174+
repo: context.repo.repo,
175+
issue_number: prNumber
176+
});
177+
178+
const hasClaRequired = labels.some(label => label.name === 'cla-required');
179+
180+
if (!hasClaRequired) {
181+
console.log('PR does not have cla-required label, skipping...');
182+
return;
183+
}
184+
185+
// Remove blocking labels and add cla-signed label
186+
try {
187+
await github.rest.issues.removeLabel({
188+
owner: context.repo.owner,
189+
repo: context.repo.repo,
190+
issue_number: prNumber,
191+
name: 'cla-required'
192+
});
193+
} catch (e) {
194+
// Label not found or already removed
195+
}
196+
197+
try {
198+
await github.rest.issues.removeLabel({
199+
owner: context.repo.owner,
200+
repo: context.repo.repo,
201+
issue_number: prNumber,
202+
name: 'integrations-with-image-change'
203+
});
204+
} catch (e) {
205+
// Label not found or already removed
206+
}
207+
208+
// Add cla-signed label
209+
await github.rest.issues.addLabels({
210+
owner: context.repo.owner,
211+
repo: context.repo.repo,
212+
issue_number: prNumber,
213+
labels: ['cla-signed']
214+
});
215+
216+
// Store the approval information for the next step
217+
core.setOutput('pr_number', prNumber);
218+
core.setOutput('pr_author', prAuthor);
219+
core.setOutput('approved_by', approvedBy);
220+
221+
console.log(`Outputs set - pr_number: ${prNumber}, pr_author: ${prAuthor}, approved_by: ${approvedBy}`);
222+
223+
// Check if confirmation comment already exists
224+
const comments = await github.rest.issues.listComments({
225+
issue_number: prNumber,
226+
owner: context.repo.owner,
227+
repo: context.repo.repo,
228+
});
229+
230+
const confirmationExists = comments.data.some(comment =>
231+
(comment.user.login === 'github-actions[bot]' || comment.user.type === 'Bot') &&
232+
comment.body.includes('Trademark addendum agreement confirmed ✅')
233+
);
234+
235+
const method = isCommentApproval ? 'Self-signed agreement' :
236+
(context.eventName === 'workflow_dispatch' ? 'Manual approval' : 'Label approval');
237+
238+
if (!confirmationExists) {
239+
await github.rest.issues.createComment({
240+
owner: context.repo.owner,
241+
repo: context.repo.repo,
242+
issue_number: prNumber,
243+
body: `## Trademark license agreement confirmed
244+
245+
The trademark license agreement has been approved for @${prAuthor}.
246+
247+
**Status:** Approved
248+
**Date:** ${new Date().toISOString()}
249+
**Approved by:** @${approvedBy}
250+
**Method:** ${method}
251+
252+
This PR is now unblocked and can proceed with normal review!`
253+
});
254+
}
255+
256+
console.log(`CLA approved for ${prAuthor} by ${approvedBy} via ${method}`)
257+
258+
- name: Record manual CLA approval
259+
if: success() && steps.process-cla-approval.outputs.pr_number != ''
260+
run: |
261+
echo "=== DEBUG: Record manual CLA approval step starting ==="
262+
echo "Available outputs:"
263+
echo " pr_number: '${{ steps.process-cla-approval.outputs.pr_number }}'"
264+
echo " pr_author: '${{ steps.process-cla-approval.outputs.pr_author }}'"
265+
echo " approved_by: '${{ steps.process-cla-approval.outputs.approved_by }}'"
266+
267+
# Ensure signatures file exists
268+
if [ ! -f "cla-signatures.json" ]; then
269+
echo '{"signatures": []}' > cla-signatures.json
270+
echo "Created new cla-signatures.json file"
271+
else
272+
echo "cla-signatures.json already exists"
273+
fi
274+
275+
# Extract approval details from previous step outputs
276+
USERNAME="${{ steps.process-cla-approval.outputs.pr_author }}"
277+
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
278+
PR_NUMBER="${{ steps.process-cla-approval.outputs.pr_number }}"
279+
APPROVED_BY="${{ steps.process-cla-approval.outputs.approved_by }}"
280+
281+
echo "Recording manual trademark addendum approval:"
282+
echo " Username: $USERNAME"
283+
echo " PR Number: $PR_NUMBER"
284+
echo " Approved by: $APPROVED_BY"
285+
echo " Date: $DATE"
286+
287+
# Check if this user already has a signature for this PR
288+
EXISTING_SIGNATURE=$(jq --arg user "$USERNAME" --arg pr "$PR_NUMBER" '.signatures[] | select(.username == $user and .pr_number == ($pr | tonumber))' cla-signatures.json)
289+
290+
if [ -z "$EXISTING_SIGNATURE" ]; then
291+
# Add new signature entry
292+
jq --arg user "$USERNAME" \
293+
--arg date "$DATE" \
294+
--arg pr "$PR_NUMBER" \
295+
--arg approved_by "$APPROVED_BY" \
296+
'.signatures += [{
297+
"username": $user,
298+
"date": $date,
299+
"pr_number": ($pr | tonumber),
300+
"approved_by": $approved_by
301+
}]' cla-signatures.json > tmp.json && mv tmp.json cla-signatures.json
302+
303+
echo "New trademark adendum signature added"
304+
else
305+
echo "Signature already exists for this user and PR"
306+
fi
307+
308+
# Commit the updated file
309+
git config user.name "github-actions[bot]"
310+
git config user.email "github-actions[bot]@users.noreply.github.com"
311+
312+
# Configure git to use the token for authentication
313+
TOKEN="${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }}"
314+
git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git"
315+
316+
git add cla-signatures.json
317+
git commit -m "Add manual approval for @$USERNAME (PR #$PR_NUMBER) by @$APPROVED_BY" || echo "No changes to commit"
318+
git push
319+
320+
echo "Manual trademark addendum approval recorded successfully"

0 commit comments

Comments
 (0)