diff --git a/.github/workflows/check-contributor.yml b/.github/workflows/check-contributor.yml new file mode 100644 index 0000000000..b4b7d2b78c --- /dev/null +++ b/.github/workflows/check-contributor.yml @@ -0,0 +1,77 @@ +name: Check Contributor + +# Checks that the input user exists in the approvers section +# of a given OWNERS file. +# +# Returns true/false at the is-repo-owner output. +# +# Can be configured to fail altogether for contexts where it makes sense, +# but in cases where this needs to return a green check mark, it is the +# the caller's responsibility to evaluate the is-repo-owner output to inform +# whether to proceed with subsequent tasks. +# +# Intended for use with workflows triggered by pull_request_target (or similar) +# events. + +on: + workflow_call: + inputs: + user: + type: string + required: true + description: + the user to evaluate + fail-workflow-if-not: + type: boolean + required: false + default: false + description: | + fails this workflow if the contributor is not an owner, + or the evaluation fails for any other reason + outputs: + is-repo-owner: # 'true' / 'false' + description: whether the input user is a repo owner + value: ${{ jobs.check-contributor.outputs.is-repo-owner }} +jobs: + check-contributor: + outputs: + is-repo-owner: ${{ steps.populate-output.outputs.is-repo-owner }} + name: Contributor is repo owner + runs-on: ubuntu-20.04 + steps: + - name: Checkout repository base + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install CI Scripts + run: | + # set up python scripts + echo "set up python script in $PWD" + python3 -m venv ve1 + cd scripts + ../ve1/bin/pip3 install -r requirements.txt + ../ve1/bin/pip3 install . + cd .. + + - name: Check contributor + # The return code from this script is what's important in this workflow. + id: check-contributor + continue-on-error: true + run: | + ./ve1/bin/user-is-repo-owner ${{ inputs.user }} + + - name: Add result to output + id: populate-output + # Outcome is the result of the workflow before continue-on-error is applied. + run: | + echo "is-repo-owner=${{ steps.check-contributor.outcome == 'success' }}" >> $GITHUB_OUTPUT + + - name: Fail if requested and the user is not a repo owner + if: inputs.fail-workflow-if-not && steps.populate-output.outputs.is-repo-owner != 'true' + run: | + echo "::error::Workflow is failing at the caller's request." + exit -1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f4a9b3135..d52c49bb21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,11 +25,19 @@ on: default: "" jobs: + check-contributor: + name: Check contributor + uses: ./.github/workflows/check-contributor.yml + with: + user: ${{ github.event.pull_request.user.login }} + workflow-test: name: Workflow Test + needs: [check-contributor] runs-on: ubuntu-20.04 if: | - github.event.pull_request.draft == false + github.event.pull_request.draft == false && + needs.check-contributor.outputs.is-repo-owner == 'true' steps: - name: Checkout uses: actions/checkout@v3 diff --git a/scripts/setup.cfg b/scripts/setup.cfg index 059ac5c1ca..ebc5ae9d18 100644 --- a/scripts/setup.cfg +++ b/scripts/setup.cfg @@ -46,4 +46,5 @@ console_scripts = get-verify-params = report.get_verify_params:main pushowners=metrics.pushowners:main update-index=updateindex.updateindex:main + user-is-repo-owner=owners.user_is_repo_owner:main diff --git a/scripts/src/owners/user_is_repo_owner.py b/scripts/src/owners/user_is_repo_owner.py new file mode 100644 index 0000000000..ed2e532f7b --- /dev/null +++ b/scripts/src/owners/user_is_repo_owner.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +"""A quick way to check if a given user is an approver in the repository's OWNERS file. + +Accepts only a single value (the username) + +Returns 0 if the user is found in the OWNERS file in the approver section. +Returns 1 if the user is NOT found in the OWNERS file. +Any other non-zero is considered a failed execution (contextually, something broke) +""" + +import os +import sys +import yaml + +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + +OWNERS_FILE = "OWNERS" + + +def is_approver(username: str) -> bool: + """Returns true if username is in the OWNERS file + + Raises an Exception in cases where the content from the OWNERS file + does not match our expectations. + """ + + with open(OWNERS_FILE, "r") as f: + data = f.read() + out = yaml.load(data, Loader=Loader) + + if "approvers" not in out: + raise Exception('OWNERS file did not have the required "approvers" key') + + approvers = out.get("approvers") + if not isinstance(approvers, list): + raise Exception('The "approvers" key was not a list, and a list is expected') + + if username in out.get("approvers"): + return True + + return False + + +def main(): + if len(sys.argv) != 2: + print( + "[Error] This script accepts only a single string as an argument, representing the user to check." + ) + return 10 + + user = sys.argv[1] + + print(f"[Info] Checking OWNERS file at path {os.path.abspath(OWNERS_FILE)}") + if not os.path.exists(OWNERS_FILE): + print(f"{OWNERS_FILE} file does not exist.") + return 20 + + try: + if is_approver(user): + print(f'[Info] User "{user}" is an approver.') + return 0 + except Exception as e: + print(f"[Error] Could not extract expected values from OWNERS file: {e}.") + return 30 + + print(f'[Info] User "{user}" is NOT an approver.') + return 1 + + +if __name__ == "__main__": + sys.exit(main())