-
Notifications
You must be signed in to change notification settings - Fork 1.8k
341 lines (312 loc) · 17.2 KB
/
Copy pathcla-check.yml
File metadata and controls
341 lines (312 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# Posts the required `verification/cla-signed` status on pull requests.
#
# Runs on pull_request_target rather than pull_request specifically so it has a
# write-capable, base-repo GITHUB_TOKEN even on fork PRs -- pull_request would hand a
# fork PR only a read-only token, unable to post a commit status at all. Because that
# grants base-repo write access to a PR-triggered run, this workflow never checks out
# the repository and never executes anything the pull request supplies: it only reads
# pull-request and commit metadata through the API, then calls a composite action
# that queries an external endpoint using already-public GitHub handles. No fork
# secret is used -- only this workflow's own token.
#
# Every job also syncs the cla-signed / cla-not-signed labels itself, right after
# posting the status: a commit status created with this workflow's own GITHUB_TOKEN
# does not fire the `status` event, so cla-label-sync.yml's primary `on: status`
# trigger never runs for a status this workflow posts. Reconciling the labels here,
# from the same write-capable token that already posted the status, keeps labeling in
# lockstep with the status this workflow controls rather than depending on a webhook
# that GitHub's own anti-recursion rule for GITHUB_TOKEN never delivers.
name: CLA check
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- ready_for_review
issue_comment:
types:
- created
jobs:
cla-check:
# Scoped to its own trigger now that the workflow has a second one (issue_comment):
# context.payload.pull_request only exists on the pull_request_target event, so
# without this guard an issue_comment event (which carries issue/comment, not
# pull_request) would also activate this job and fail resolving the PR.
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
permissions:
statuses: write
pull-requests: write
contents: read
steps:
- name: Enumerate PR committers
id: enumerate
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
// --- enumeration transform (start) -------------------------------------
// Pure transform from `commits` + `pr.commits` only. Mirrored 1:1 by a
// standalone unit test elsewhere, since this workflow has no checkout
// step to require() this logic from live -- keep it self-contained here
// so that mirror stays easy to verify faithful.
//
// The pull-commits API hard-caps what a single (paginated) call can
// return. If the PR has more commits than were returned, some authors
// are invisible to us, so completeness must fail closed rather than
// silently treat a partial read as the full set.
const enumerationComplete = pr.commits <= commits.length;
const logins = [...new Set(commits.map(c => c.author && c.author.login).filter(Boolean))];
const unidentified = commits.filter(c => !(c.author && c.author.login)).map(c => c.sha);
// --- enumeration transform (end) ---------------------------------------
core.info(
`PR #${pr.number}: enumerated ${commits.length}/${pr.commits} commit(s), ` +
`${logins.length} login(s), ${unidentified.length} unidentified, ` +
`enumeration-complete=${enumerationComplete}`
);
core.setOutput('head-sha', pr.head.sha);
core.setOutput('logins', JSON.stringify(logins));
core.setOutput('enumeration-complete', enumerationComplete ? 'true' : 'false');
core.setOutput('unidentified', JSON.stringify(unidentified));
- name: Check CLA status and post verification/cla-signed
id: cla-status
# Referenced by repo path@ref, not a local `./` path, so this step resolves
# with no checkout: the runner fetches the action directly from the given
# ref, independent of this job's (empty) workspace. This also means the
# call always tracks develop's current tip at run time rather than a
# commit frozen to this workflow file -- acceptable for a same-repo
# self-reference to code that changes on the same cadence as this file.
uses: fivetran/great_expectations/.github/actions/cla-status@develop
with:
status-sha: ${{ steps.enumerate.outputs.head-sha }}
logins: ${{ steps.enumerate.outputs.logins }}
enumeration-complete: ${{ steps.enumerate.outputs.enumeration-complete }}
unidentified: ${{ steps.enumerate.outputs.unidentified }}
unidentified-policy: fail
token: ${{ github.token }}
- name: Sync CLA labels to this check's own result
uses: fivetran/great_expectations/.github/actions/cla-label-sync@develop
with:
pr-number: ${{ github.event.pull_request.number }}
state: ${{ steps.cla-status.outputs.state }}
token: ${{ github.token }}
- name: Build guiding comment body
id: build-comment
if: steps.cla-status.outputs.state == 'error'
uses: actions/github-script@v7
env:
CLA_UNSIGNED: ${{ steps.cla-status.outputs.unsigned }}
CLA_UNIDENTIFIED: ${{ steps.cla-status.outputs.unidentified }}
with:
script: |
// --- guiding-comment body transform (start) ----------------------------
// Mirrored 1:1 in the cla-recheck job below, for the same reason the
// enumeration transform above is: no checkout step here to require()
// shared logic from live.
const unsigned = JSON.parse(process.env.CLA_UNSIGNED || '[]');
const unidentified = JSON.parse(process.env.CLA_UNIDENTIFIED || '[]');
const sections = [];
if (unsigned.length > 0) {
const mentions = unsigned.map((login) => `@${login}`).join(', ');
sections.push(
`We could not find a signed CLA for: ${mentions}. Please sign the ` +
'[Individual Contributor License Agreement](https://forms.gle/wvregSivqgAaJNEX8), ' +
'or the [Software Grant and Corporate Contributor License Agreement]' +
'(https://forms.gle/6viSVNxZjui9Vhi29) if you are contributing on behalf of your ' +
'employer (see [CLA.md](https://github.com/fivetran/great_expectations/blob/develop/CLA.md) ' +
'for details).'
);
}
if (unidentified.length > 0) {
const refs = unidentified.map((sha) => `\`${String(sha).substring(0, 7)}\``).join(', ');
sections.push(
`We were unable to identify the GitHub account for the following commit(s): ${refs}. ` +
'Please make sure the email address on your commits is linked to your GitHub account.'
);
}
if (sections.length === 0) {
// Enumeration-incomplete case: state is `error` with neither list
// populated. Keep the comment non-empty and honest about why.
sections.push('We were unable to fully verify the CLA status for this pull request.');
}
const body = [
'Thank you for your contribution! Before we can merge this pull request, every ' +
'committer needs to have signed our Contributor License Agreement (CLA).',
...sections,
'Once resolved, comment `@cla-bot check` on this pull request to re-run the check.',
].join('\n\n');
core.setOutput('body', body);
// --- guiding-comment body transform (end) ------------------------------
- name: Post or update unsigned-CLA guiding comment
if: steps.cla-status.outputs.state == 'error'
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: cla-check
pr_number: ${{ github.event.pull_request.number }}
message: ${{ steps.build-comment.outputs.body }}
- name: Resolve guiding comment once the CLA is signed
if: steps.cla-status.outputs.state == 'success'
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: cla-check
# Only update a comment that's already there -- a PR that was fully
# signed from its first run never had an unsigned comment to resolve,
# so there's nothing to say here.
create_if_not_exists: false
pr_number: ${{ github.event.pull_request.number }}
message: 'All committers have signed the CLA. :white_check_mark:'
cla-recheck:
# Lets a committer re-trigger the check after signing, without pushing a new commit
# (e.g. a commit made before signing would otherwise never get re-evaluated). Scoped
# to the exact recognized phrase so it can't be triggered by unrelated PR discussion,
# and to comments on a pull request specifically (issue_comment fires for both issues
# and PR comments; `issue.pull_request` is only set for the latter).
if: >-
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
github.event.comment.body == '@cla-bot check'
runs-on: ubuntu-latest
permissions:
statuses: write
pull-requests: write
contents: read
steps:
- name: Resolve PR and re-enumerate committers
id: enumerate
uses: actions/github-script@v7
with:
script: |
const pr = (await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.issue.number,
})).data;
// A comment can arrive after the PR closed or merged. There's nothing to
// re-check at that point -- exit cleanly rather than posting a status update
// to a PR that's no longer open for one.
if (pr.state !== 'open') {
core.info(`PR #${pr.number} is ${pr.state}; recheck is a no-op.`);
core.setOutput('skip', 'true');
return;
}
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
// --- enumeration transform (start) -------------------------------------
// Pure transform from `commits` + `pr.commits` only. Mirrored 1:1 by a
// standalone unit test elsewhere, since this workflow has no checkout
// step to require() this logic from live -- keep it self-contained here
// so that mirror stays easy to verify faithful.
//
// The pull-commits API hard-caps what a single (paginated) call can
// return. If the PR has more commits than were returned, some authors
// are invisible to us, so completeness must fail closed rather than
// silently treat a partial read as the full set.
const enumerationComplete = pr.commits <= commits.length;
const logins = [...new Set(commits.map(c => c.author && c.author.login).filter(Boolean))];
const unidentified = commits.filter(c => !(c.author && c.author.login)).map(c => c.sha);
// --- enumeration transform (end) ---------------------------------------
core.info(
`PR #${pr.number}: enumerated ${commits.length}/${pr.commits} commit(s), ` +
`${logins.length} login(s), ${unidentified.length} unidentified, ` +
`enumeration-complete=${enumerationComplete}`
);
core.setOutput('skip', 'false');
core.setOutput('head-sha', pr.head.sha);
core.setOutput('logins', JSON.stringify(logins));
core.setOutput('enumeration-complete', enumerationComplete ? 'true' : 'false');
core.setOutput('unidentified', JSON.stringify(unidentified));
- name: Check CLA status and post verification/cla-signed
id: cla-status
if: steps.enumerate.outputs.skip == 'false'
# Same repo-path@ref reference as the pull_request_target job, for the same
# reason: this job also has no checkout step, so a local `./` action path would
# not resolve.
uses: fivetran/great_expectations/.github/actions/cla-status@develop
with:
status-sha: ${{ steps.enumerate.outputs.head-sha }}
logins: ${{ steps.enumerate.outputs.logins }}
enumeration-complete: ${{ steps.enumerate.outputs.enumeration-complete }}
unidentified: ${{ steps.enumerate.outputs.unidentified }}
unidentified-policy: fail
token: ${{ github.token }}
- name: Sync CLA labels to this check's own result
if: steps.enumerate.outputs.skip == 'false'
uses: fivetran/great_expectations/.github/actions/cla-label-sync@develop
with:
pr-number: ${{ github.event.issue.number }}
state: ${{ steps.cla-status.outputs.state }}
token: ${{ github.token }}
- name: Build guiding comment body
id: build-comment
if: steps.enumerate.outputs.skip == 'false' && steps.cla-status.outputs.state == 'error'
uses: actions/github-script@v7
env:
CLA_UNSIGNED: ${{ steps.cla-status.outputs.unsigned }}
CLA_UNIDENTIFIED: ${{ steps.cla-status.outputs.unidentified }}
with:
script: |
// --- guiding-comment body transform (start) ----------------------------
// Mirrored 1:1 from the cla-check job above, for the same reason the
// enumeration transform is: no checkout step here to require() shared
// logic from live.
const unsigned = JSON.parse(process.env.CLA_UNSIGNED || '[]');
const unidentified = JSON.parse(process.env.CLA_UNIDENTIFIED || '[]');
const sections = [];
if (unsigned.length > 0) {
const mentions = unsigned.map((login) => `@${login}`).join(', ');
sections.push(
`We could not find a signed CLA for: ${mentions}. Please sign the ` +
'[Individual Contributor License Agreement](https://forms.gle/wvregSivqgAaJNEX8), ' +
'or the [Software Grant and Corporate Contributor License Agreement]' +
'(https://forms.gle/6viSVNxZjui9Vhi29) if you are contributing on behalf of your ' +
'employer (see [CLA.md](https://github.com/fivetran/great_expectations/blob/develop/CLA.md) ' +
'for details).'
);
}
if (unidentified.length > 0) {
const refs = unidentified.map((sha) => `\`${String(sha).substring(0, 7)}\``).join(', ');
sections.push(
`We were unable to identify the GitHub account for the following commit(s): ${refs}. ` +
'Please make sure the email address on your commits is linked to your GitHub account.'
);
}
if (sections.length === 0) {
// Enumeration-incomplete case: state is `error` with neither list
// populated. Keep the comment non-empty and honest about why.
sections.push('We were unable to fully verify the CLA status for this pull request.');
}
const body = [
'Thank you for your contribution! Before we can merge this pull request, every ' +
'committer needs to have signed our Contributor License Agreement (CLA).',
...sections,
'Once resolved, comment `@cla-bot check` on this pull request to re-run the check.',
].join('\n\n');
core.setOutput('body', body);
// --- guiding-comment body transform (end) ------------------------------
- name: Post or update unsigned-CLA guiding comment
if: steps.enumerate.outputs.skip == 'false' && steps.cla-status.outputs.state == 'error'
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: cla-check
pr_number: ${{ github.event.issue.number }}
message: ${{ steps.build-comment.outputs.body }}
- name: Resolve guiding comment once the CLA is signed
if: steps.enumerate.outputs.skip == 'false' && steps.cla-status.outputs.state == 'success'
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: cla-check
# Only update a comment that's already there -- a PR that was fully
# signed from its first run never had an unsigned comment to resolve,
# so there's nothing to say here.
create_if_not_exists: false
pr_number: ${{ github.event.issue.number }}
message: 'All committers have signed the CLA. :white_check_mark:'