Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate CLA checking #11783

Merged
merged 16 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions .github/actions/check-for-CLA/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Octokit } from "@octokit/core";
import { google } from "googleapis";
import Handlebars from "handlebars";
import fs from "fs-extra";

const PULL_REQUST_INFO = {
id: process.env.PULL_REQUEST_ID,
owner: process.env.GITHUB_REPOSITORY.split("/")[0],
repoName: process.env.GITHUB_REPOSITORY.split("/")[1],
username: process.env.GITHUB_ACTOR,
gitHubToken: process.env.GITHUB_TOKEN,
};

const GOOGLE_SHEETS_INFO = {
APIKeys: process.env.GOOGLE_KEYS,
individualCLASheetId: process.env.INDIVIDUAL_CLA_SHEET_ID,
corporateCLASheetId: process.env.CORPORATE_CLA_SHEET_ID,
};

const CONTRIBUTORS_URL =
"https://github.com/CesiumGS/cesium/blob/main/CONTRIBUTORS.md";

const main = async () => {
let hasSignedCLA;
let errorFoundOnCLACheck;

try {
hasSignedCLA = await checkIfUserHasSignedAnyCLA();
} catch (error) {
errorFoundOnCLACheck = error.toString();
}

const response = await postCommentOnPullRequest(
hasSignedCLA,
errorFoundOnCLACheck
);
};

const checkIfUserHasSignedAnyCLA = async () => {
let foundIndividualCLA = await checkIfIndividualCLAFound();
if (foundIndividualCLA) {
return true;
}

let foundCorporateCLA = await checkIfCorporateCLAFound();
return foundCorporateCLA;
};

const checkIfIndividualCLAFound = async () => {
const response = await getValuesFromGoogleSheet(
GOOGLE_SHEETS_INFO.individualCLASheetId,
"D2:D"
);

const rows = response.data.values;
for (let i = 0; i < rows.length; i++) {
if (rows[i].length === 0) {
continue;
}

const rowUsername = rows[i][0].toLowerCase();
if (PULL_REQUST_INFO.username.toLowerCase() === rowUsername) {
return true;
}
}

return false;
};

const checkIfCorporateCLAFound = async () => {
const response = await getValuesFromGoogleSheet(
GOOGLE_SHEETS_INFO.corporateCLASheetId,
"H2:H"
);

const rows = response.data.values;
for (let i = 0; i < rows.length; i++) {
if (rows[i].length === 0) {
continue;
}

// We're more lenient with the ScheduleA username check since it's an unformatted text field.
let rowScheduleA = rows[i][0].toLowerCase();
rowScheduleA = rowScheduleA.replace(/\n/g, " ");
const words = rowScheduleA.split(" ");

for (let j = 0; j < words.length; j++) {
// Checking for substrings because many GitHub usernames added as "github.com/username".
if (words[j].includes(PULL_REQUST_INFO.username.toLowerCase())) {
return true;
}
}
}

return false;
};

const getValuesFromGoogleSheet = async (sheetId, cellRanges) => {
const googleSheetsApi = await getGoogleSheetsApiClient();

return googleSheetsApi.spreadsheets.values.get({
spreadsheetId: sheetId,
range: cellRanges,
});
};

const getGoogleSheetsApiClient = async () => {
const googleConfigFilePath = "GoogleConfig.json";
fs.writeFileSync(googleConfigFilePath, GOOGLE_SHEETS_INFO.APIKeys);

const auth = new google.auth.GoogleAuth({
keyFile: googleConfigFilePath,
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const googleAuthClient = await auth.getClient();

return google.sheets({ version: "v4", auth: googleAuthClient });
};

const postCommentOnPullRequest = async (hasSignedCLA, errorFoundOnCLACheck) => {
const octokit = new Octokit();

return octokit.request(
`POST /repos/${PULL_REQUST_INFO.owner}/${PULL_REQUST_INFO.repoName}/issues/${PULL_REQUST_INFO.id}/comments`,
{
owner: PULL_REQUST_INFO.username,
repo: PULL_REQUST_INFO.repoName,
issue_number: PULL_REQUST_INFO.id,
body: getCommentBody(hasSignedCLA, errorFoundOnCLACheck),
headers: {
authorization: `bearer ${PULL_REQUST_INFO.gitHubToken}`,
accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
};

const getCommentBody = (hasSignedCLA, errorFoundOnCLACheck) => {
const commentTemplate = fs.readFileSync(
"./.github/actions/check-for-CLA/templates/pullRequestComment.hbs",
"utf-8"
);

const getCommentFromTemplate = Handlebars.compile(commentTemplate);
const commentBody = getCommentFromTemplate({
errorCla: errorFoundOnCLACheck,
hasCla: hasSignedCLA,
username: PULL_REQUST_INFO.username,
contributorsUrl: CONTRIBUTORS_URL,
});

return commentBody;
};

main();
20 changes: 20 additions & 0 deletions .github/actions/check-for-CLA/templates/pullRequestComment.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{#if errorCla}}
:red_circle: There was an error checking the CLA! If this is your first contribution, please send in a [Contributor License Agreement](https://github.com/CesiumGS/cesium/blob/main/CONTRIBUTING.md#contributor-license-agreement-cla).
```
{{ errorCla }}
```
{{else}}
{{#if hasCla}}
Thank you for the pull request, @{{ username }}!

:white_check_mark: We can confirm we have a CLA on file for you.
{{else}}
Thank you for the pull request, @{{ username }}! Welcome to the Cesium community!

In order for us to review your PR, please complete the following steps:
- [ ] Send in a [Contributor License Agreement](https://github.com/CesiumGS/cesium/blob/main/CONTRIBUTING.md#contributor-license-agreement-cla) (CLA)
- [ ] Add yourself to the [contributors]({{ contributorsUrl }}) file

Review [Pull Request Guidelines](https://github.com/CesiumGS/cesium/blob/main/CONTRIBUTING.md#pull-request-guidelines) to make sure your PR gets accepted quickly.
{{/if}}
{{/if}}
26 changes: 26 additions & 0 deletions .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CLA Checking
on:
pull_request:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just want the open action to trigger this, right? By default, pull_request includes other triggers as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad! I think I didn't catch this because it works similar to how we want by default:

For example, if no activity types are specified, the workflow runs when a pull request is opened or reopened or when the head branch of the pull request is updated.

But good to have this only when a new PR created. Fixing in next commit.


jobs:
check-cla:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v3
- name: install node 20
uses: actions/setup-node@v3
with:
node-version: '20'
- name: install npm packages
run: npm install googleapis @octokit/core handlebars fs-extra
- name: run script
run: node .github/actions/check-for-CLA/index.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST_ID: ${{ github.event.number }}
GOOGLE_KEYS: ${{ secrets.GOOGLE_KEYS }}
INDIVIDUAL_CLA_SHEET_ID: ${{ secrets.INDIVIDUAL_CLA_SHEET_ID }}
CORPORATE_CLA_SHEET_ID: ${{ secrets.CORPORATE_CLA_SHEET_ID }}
2 changes: 1 addition & 1 deletion .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ jobs:
run: npm pack &> /dev/null
- name: package workspace modules
run: npm pack --workspaces &> /dev/null
- uses: ./.github/actions/verify-package
- uses: ./.github/actions/verify-package
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ yarn.lock
.idea/workspace.xml
.idea/tasks.xml
.idea/shelf

# Used in the CLA checking GitHub workflow
GoogleConfig.json
Loading