From 9b67eafd5ae273a677717fd6f34fb57ca038b6cc Mon Sep 17 00:00:00 2001 From: psdhanesh7 Date: Wed, 11 May 2022 08:47:07 +0530 Subject: [PATCH] Add CLA checks --- .github/workflows/comment_pr.yml | 66 +++++++ .../python/verify_cla_signature_pr.py | 177 ++++++++++++++++++ .github/workflows/verify_cla_signature_pr.yml | 74 ++++++++ 3 files changed, 317 insertions(+) create mode 100644 .github/workflows/comment_pr.yml create mode 100644 .github/workflows/python/verify_cla_signature_pr.py create mode 100644 .github/workflows/verify_cla_signature_pr.yml diff --git a/.github/workflows/comment_pr.yml b/.github/workflows/comment_pr.yml new file mode 100644 index 00000000..e029b352 --- /dev/null +++ b/.github/workflows/comment_pr.yml @@ -0,0 +1,66 @@ +name: Comment on pull request post pull request auto review +# depends on and only executes after pull_request_processor.yml is complete + +on: + workflow_run: + workflows: ["Checking CLA Signature"] + types: + - completed + +jobs: + one: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + if: > + ${{ github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Print github contexts + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: | + ls + echo "GH context:" + echo "$GITHUB_CONTEXT" + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + console.log(artifacts); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "prcontext" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data)); + - run: | + ls + unzip pr.zip + cat ./comment + + - name: 'Comment on Current Pull Request' + uses: actions/github-script@v3 + with: + github-token: ${{ secrets.COMMENT_BOT_TOKEN }} + script: | + var fs = require('fs'); + var issue_number = Number(fs.readFileSync('./PRNumber')); + var comment = String(fs.readFileSync('./comment')); + await github.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + body: comment + }); \ No newline at end of file diff --git a/.github/workflows/python/verify_cla_signature_pr.py b/.github/workflows/python/verify_cla_signature_pr.py new file mode 100644 index 00000000..2b624948 --- /dev/null +++ b/.github/workflows/python/verify_cla_signature_pr.py @@ -0,0 +1,177 @@ +import os +import sys +import requests +import json +import subprocess +import re + + +print("current working directory is: ", os.getcwd()) +STATUS_FAILED = 'FAILED' +SUCCESS_MESSAGE = 'ok' + + +def get_github_details(): + github_info_file = open('./.tmp/github.json', 'r') + return json.load(github_info_file) + + +def get_commit_details(): + commit_info_file = open('./.tmp/commitDetails.json', 'r') + return json.load(commit_info_file) + + +def process_git_local_details(): + # Check if current dir is git dir + is_git_dir = subprocess.check_output( + ['/usr/bin/git', 'rev-parse', '--is-inside-work-tree']).decode('utf-8') + print("Is git dir: ", is_git_dir) + + # git status + git_status = subprocess.check_output( + ['/usr/bin/git', 'status']).decode('utf-8') + print("Git status: ", git_status) + + # last n commits + last_10_commit_list = subprocess.check_output( + ['/usr/bin/git', 'rev-list', '--max-count=10', 'HEAD']).decode('utf-8') + print("last 10 commit ids are: ", last_10_commit_list) + + return { + 'is_git_dir': is_git_dir, + 'last_10_commit_list': last_10_commit_list + } + + +def collect_pr_details(): + github = get_github_details() + commits = get_commit_details() + git_local = process_git_local_details() + return { + 'github': github, + 'commits': commits, + 'num_commits_in_pr': len(commits), + 'event_name': github["event_name"], + 'pr_submitter_github_login': github['event']['pull_request']['user']['login'], + 'github_repo': github['repository'], + 'pr_number': github['event']['number'], + 'is_git_dir': git_local['is_git_dir'], + 'last_10_commit_list': git_local['last_10_commit_list'], + } + + +def write_comment(comment): + print(comment) + f = open("./.tmp/comment", "a") + f.write(comment) + f.write("\n") + f.close() + + +def task_failed(comment): + f = open("./.tmp/failed", "a") + f.write(comment) + f.write("\n") + f.close() + write_comment(comment) + return STATUS_FAILED + + +def extract_personal_contributer_details(): + personal_cla_link = sys.argv[1] + f = requests.get(personal_cla_link) + personal_cla_contents = f.text + + personal_contributers_regex = re.compile('\| *\[([^\s]+)\]\([^\s]+\) *\|') + personal_contributers = personal_contributers_regex.findall( + personal_cla_contents) + + return personal_contributers + + +def extract_employer_contributer_details(): + employer_cla_link = sys.argv[2] + f = requests.get(employer_cla_link) + employer_cla_contents = f.text + + employer_contributers_regex = re.compile('\| *\[([^\s]+)\]\([^\s]+\) *\|') + employer_contributers = employer_contributers_regex.findall( + employer_cla_contents) + + return employer_contributers + + +def validate_is_pull_request(pr_details): + print('Validate pull request called') + github_details = pr_details['github'] + if github_details["event_name"] != "pull_request": + print("Error! This operation is valid on github pull requests. Exiting. Event received: ", + github_details["event_name"]) + sys.exit(1) + + +def validate_pr_raiser_cla(pr_raiser_login, employer_contributors, personal_contributors): + print('PR raiser login: ' + pr_raiser_login) + if pr_raiser_login not in employer_contributors and pr_raiser_login not in personal_contributors: + return task_failed('### Error: Contributor Licence Agreement Signature Missing\n' + + 'Please sign the Contributor Licence Agreement by clicking the following link.\n' + + '

Click here to sign the CLA

' + ) + print('Pass: Pull request raiser has signed the Contributor Licence Agreement') + return SUCCESS_MESSAGE + + +def validate_commiters_cla(commits, employer_contributors, personal_contributors): + # github logins of all committers + commit_logins = [] + for commit in commits: + commiter_github_login = commit['author']['login'] + if commiter_github_login not in commit_logins: + commit_logins.append(commiter_github_login) + print("All github users who made changes to the pull request: ", commit_logins) + + unauthorized_commiters = [] + for user in commit_logins: + if user not in personal_contributors and user not in employer_contributors: + unauthorized_commiters.append(user) + if len(unauthorized_commiters) != 0: + return task_failed('### Error: Contributor Licence Agreement Signature Missing\n' + + 'The following commiter(s) has not signed the Contributor Licence Agreement:\n' + + ', '.join(unauthorized_commiters) + '\n' + + 'Please sign the Contributor Licence Agreement by clicking the following link. \n' + + '

Click here to sign the CLA

' + ) + + print('Pass: All the commiters have signed the Contributor Licence Agreement') + return SUCCESS_MESSAGE + + +def validate_cla_signature(pr_raiser_login, commits): + employer_contributors = extract_employer_contributer_details() + personal_contributors = extract_personal_contributer_details() + + PR_RAISER_CLA_VALIDATION = validate_pr_raiser_cla(pr_raiser_login, employer_contributors, personal_contributors) + COMMITERS_CLA_VALIDATION = validate_commiters_cla(commits, employer_contributors, personal_contributors) + + if PR_RAISER_CLA_VALIDATION == STATUS_FAILED or COMMITERS_CLA_VALIDATION == STATUS_FAILED: + return STATUS_FAILED + + return SUCCESS_MESSAGE + + +def review_pr(): + print('Reviewing PR') + pr_details = collect_pr_details() + validate_is_pull_request(pr_details) + CLA_SIGNATURE_VALIDATION = validate_cla_signature(pr_details['pr_submitter_github_login'], pr_details['commits']) + + if CLA_SIGNATURE_VALIDATION == STATUS_FAILED: + print('Validations failed. Exiting!') + return + + write_comment('\n## Thank You for making this pull request.') + + +review_pr() + +# assert validate_cla_signature('psdhanesh7') == SUCCESS_MESSAGE \ No newline at end of file diff --git a/.github/workflows/verify_cla_signature_pr.yml b/.github/workflows/verify_cla_signature_pr.yml new file mode 100644 index 00000000..2b7a3a4f --- /dev/null +++ b/.github/workflows/verify_cla_signature_pr.yml @@ -0,0 +1,74 @@ +name: Checking CLA Signature +# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#github-context +# The pull request target event provides RW token to github +# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/ +# But `on: pull_request_target` should be avoided due to security +# reasons. Read more: [SEC_ADV_1] https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ +# +# We will use a mix of github pull_request that does not provide any write access to pull requests on forks +# and workflow event, as discussed in [SEC_ADV_1] + +on: + pull_request: + branches: + - main + +jobs: + one: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 100 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Setting up prerequisites + run: | + mkdir ./.tmp + pip3 install requests + - name: Getting commit details + uses: wei/wget@v1 + with: + args: -O ./.tmp/commitDetails.json ${{ toJSON(github.event.pull_request._links.commits.href) }} + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: | + echo "$GITHUB_CONTEXT" > ./.tmp/github.json + echo ${{ github.event.number }} > ./.tmp/PRNumber + cat ./.tmp/github.json + echo "commit details: " + cat ./.tmp/commitDetails.json + + - name: Review pull request + run: | + which git + if ! python ./.github/workflows/python/verify_cla_signature_pr.py $PERSONAL_CLA_LINK $EMPLOYER_CLA_LINK; then + echo "Pull request details could not be extracted" + exit 1 + else + echo "all good" + fi + env: + EMPLOYER_CLA_LINK: https://raw.githubusercontent.com/phcode-dev/contributor-license-agreement/main/employer_contributor_license_agreement.md + PERSONAL_CLA_LINK: https://raw.githubusercontent.com/phcode-dev/contributor-license-agreement/main/personal_contributor_licence_agreement.md + + - uses: actions/upload-artifact@v2 + with: + name: prcontext + path: .tmp/ + + - name: Fail on validation errors + run: | + FILE=./.tmp/failed + if test -f "$FILE"; then + echo "Validation failed. Reason:" + cat ./.tmp/failed + exit 1 + fi