diff --git a/CHANGES.md b/CHANGES.md index 77d6f55..2dc78a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,20 @@ # Changelog +## 2.2.0 +* Add new ``github.add_repository_collaborator`` action which allows user to add a collaborator to repository. +* Add new ``github.check_user_repository_collaborator`` action which allows user to check if an user is a collaborator's repository. +* Add new ``github.get_repository_collaborators`` action which allows user to list the collaborators of repository. +* Add new ``github.add_update_repository_team`` action which allows user to add a team to repository. +* Add new ``github.check_team_permissions_for_repository`` action which allows user to check if a team has access to repository. +* Add new ``github.create_organization_repository`` action which allows user to create an organization repository. +* Add new ``github.create_repository_authenticated_user`` action which allows user to create a user repository. +* Add new ``github.create_repository_from_template`` action which allows user to create a repository from a template repository. +* Add new ``github.add_update_repository_environment`` action which allows user to create a repository deployment environment. +* Bug fix on ``github.store_oauth_token`` to save the token correctly so that it can be read later. +* Security improvement on ``github.store_oauth_token`` to encrypt the github token and hide it in web interface. +* Add new ``github.create_branch``, ``github.get_branch``, ``github.delete_branch`` actions which allow user to create/get/delete a branch. +* Add token to ``github.create_file``, ``github.create_pull``, ``github.update_file``. + ## 2.1.3 * Fix `update_branch_protection` action: dismissal users and teams can now be null in GitHub's API response. diff --git a/README.md b/README.md index 3bb3999..ce4cb4f 100644 --- a/README.md +++ b/README.md @@ -97,16 +97,28 @@ StackStorm webhook handler. ## Actions * ``add_comment`` - Add comment to the provided issue / pull request. +* ``add_repository_collaborator`` - Add a collaborator to repository. +* ``add_update_repository_environment`` - Add a deployment environment to a repository. * ``add_status`` - Add commit status to the provided commit. +* ``add_update_repository_team`` - Add/Update a team to repository. +* ``create_branch`` - Create new branch. * ``create_file`` - Create new file. * ``create_issue`` - Create a new issue. +* ``create_repository_authenticated_user`` - Create an user repository. +* ``create_repository_from_template`` - Create a repository from template. +* ``create_organization_repository`` - Create an organization repository. * ``create_pull`` - Create a new Pull Request. +* ``check_team_permissions_for_repository`` - Check if a team has access to repository. +* ``check_user_repository_collaborator`` - Check if an user is a collaborator's repository. +* ``delete_branch`` - Remove branch. * ``delete_branch_protection`` - Remove branch protection settings. +* ``get_branch`` - Get branch. * ``get_branch_protection`` - Get branch protection settings. * ``get_contents`` - Get repository or file contents. * ``get_issue`` - Retrieve information about a particular issue. Note: You only need to specify authentication token in the config if you use this action with a private repository. +* ``get_repository_collaborators`` - List the collaborators of repository. * ``list_issues`` - List all the issues for a particular repo (includes pull requests since pull requests are just a special type of issues). * ``list_pulls`` - List all pull requests for a particular repo. diff --git a/actions/add_repository_collaborator.py b/actions/add_repository_collaborator.py new file mode 100644 index 0000000..57f0f13 --- /dev/null +++ b/actions/add_repository_collaborator.py @@ -0,0 +1,26 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'AddRepositoryCollaboratorAction' +] + + +class AddRepositoryCollaboratorAction(BaseGithubAction): + def run(self, api_user, owner, repo, username, github_type, permission): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + payload = {"permission": permission} + + response = self._request("PUT", + "/repos/{}/{}/collaborators/{}".format(owner, repo, username), + payload, + self.token, + enterprise) + + results = {'response': response} + + return results diff --git a/actions/add_repository_collaborator.yaml b/actions/add_repository_collaborator.yaml new file mode 100644 index 0000000..c022551 --- /dev/null +++ b/actions/add_repository_collaborator.yaml @@ -0,0 +1,43 @@ +--- +name: add_repository_collaborator +runner_type: python-script +pack: github +description: > + Add a repository collaborator. + Example: + st2 run github.add_repository_collaborator owner="organization" repo="reponame" username="collaborator" api_user="token_name" +enabled: true +entry_point: add_repository_collaborator.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + owner: + type: "string" + description: "The account owner of the repository. The name is not case sensitive." + required: true + repo: + type: "string" + description: "The name of the repository. The name is not case sensitive." + required: true + username: + type: "string" + description: "The handle for the GitHub user account." + required: true + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + permission: + type: "string" + description: "The permission to grant the collaborator. Only valid on organization-owned repositories. In addition to the enumerated values, you can also specify a custom repository role name, if the owning organization has defined any." + enum: + - "pull" + - "push" + - "admin" + - "maintain" + - "triage" + default: "push" \ No newline at end of file diff --git a/actions/add_update_repository_environment.py b/actions/add_update_repository_environment.py new file mode 100644 index 0000000..d031f27 --- /dev/null +++ b/actions/add_update_repository_environment.py @@ -0,0 +1,60 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'AddUpdateRepositoryEnvironmentAction' +] + + +class AddUpdateRepositoryEnvironmentAction(BaseGithubAction): + + def _get_team_id(self, enterprise, org, name): + self.logger.debug("Getting team ID for name [%s]", name) + response = self._request("GET", + f"/orgs/{org}/teams/{name}", + None, + self.token, + enterprise) + self.logger.debug("Found ID [%d] for name [%s]", response["id"], name) + return response["id"] + + def run(self, api_user, environment, + owner, repo, github_type, reviewers, wait_timer, deployment_branch_policy): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + # Transforming team slug names in IDs + for reviewer in reviewers: + type = reviewer.get("type", None) + name = reviewer.get("name", None) + if type == "Team" and name: + del reviewer["name"] + reviewer["id"] = self._get_team_id(enterprise, owner, name) + elif type == "User" and name: + raise NotImplementedError("Providing reviewer of type user without ID is not implemented!") + + payload = { + "wait_timer": int(wait_timer), + "reviewers": reviewers, + "deployment_branch_policy": deployment_branch_policy + } + + self.logger.info( + "Adding/Updating environment [%s] with parameters [%s] for repo [%s/%s] with user [%s]", + environment, payload, owner, repo, api_user) + + try: + response = self._request("PUT", + f"/repos/{owner}/{repo}/environments/{environment}", + payload, + self.token, + enterprise) + results = {'response': response} + return results + except Exception as e: + self.logger.error("Could not add/update environment, error: %s", repr(e)) + return (False, "Could not add/update environment, error: %s" % repr(e)) + + return (False, "Could not add/update environment for unknown reason!") diff --git a/actions/add_update_repository_environment.yaml b/actions/add_update_repository_environment.yaml new file mode 100644 index 0000000..b91ad74 --- /dev/null +++ b/actions/add_update_repository_environment.yaml @@ -0,0 +1,78 @@ +--- +name: add_update_repository_environment +# https://docs.github.com/en/enterprise-server@3.2/rest/deployments/environments +runner_type: python-script +pack: github +description: > + Add or update a repository environment. + Example: + st2 run github.add_update_repository_environment owner="owner" repo="reponame" environment="test" reviewers='[{"type": "Team", "name": "test-team"}]' api_user="test" github_type=online +enabled: true +entry_point: add_update_repository_environment.py +parameters: + # Repository parameters + owner: + type: "string" + description: "The account owner of the repository. The name is not case sensitive." + required: true + repo: + type: "string" + description: "The name of the repository. The name is not case sensitive." + required: true + # Authentication parameters + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + # Call-specific parameters :) + environment: + type: string + description: "The name of the environment" + required: true + wait_timer: + type: number + description: "The amount of time to delay a job after the job is initially triggered. The time (in minutes) must be an integer between 0 and 43,200 (30 days)." + required: false + default: 0 + reviewers: + type: array + default: [] + description: "The people or teams that may review jobs that reference the environment. You can list up to six users or teams as reviewers. The reviewers must have at least read access to the repository. Only one of the required reviewers needs to approve the job for it to proceed." + required: false + items: + type: object + properties: + type: + type: string + required: true + enum: + - "User" + - "Team" + id: + # Note, you MUST provide a id if the type is User.. otherwise you may provide the team name, and the script + # will detect the ID + type: number + required: false + name: + type: string + required: false + deployment_branch_policy: + type: object + description: "The type of deployment branch policy for this environment. To allow all branches to deploy, set to null." + required: false + default: null + properties: + protected_branches: + type: boolean + required: true + description: Whether only branches with branch protection rules can deploy to this environment. If protected_branches is true, custom_branch_policies must be false; if protected_branches is false, custom_branch_policies must be true. + custom_branch_policies: + type: boolean + required: true + description: Whether only branches that match the specified name patterns can deploy to this environment. If custom_branch_policies is true, protected_branches must be false; if custom_branch_policies is false, protected_branches must be true. diff --git a/actions/add_update_repository_team.py b/actions/add_update_repository_team.py new file mode 100644 index 0000000..56a934c --- /dev/null +++ b/actions/add_update_repository_team.py @@ -0,0 +1,27 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'AddUpdateRepositoryTeamAction' +] + + +class AddUpdateRepositoryTeamAction(BaseGithubAction): + def run(self, api_user, org, team_slug, + owner, repo, github_type, permission): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + payload = {"permission": permission} + + response = self._request("PUT", + "/orgs/{}/teams/{}/repos/{}/{}".format(org, team_slug, owner, repo), + payload, + self.token, + enterprise) + + results = {'response': response} + + return results diff --git a/actions/add_update_repository_team.yaml b/actions/add_update_repository_team.yaml new file mode 100644 index 0000000..c51c61b --- /dev/null +++ b/actions/add_update_repository_team.yaml @@ -0,0 +1,47 @@ +--- +name: add_update_repository_team +runner_type: python-script +pack: github +description: > + Add or update repository team. + Example: + st2 run github.add_update_repository_team org="organization" owner="owner" repo="reponame" team_slug="team_id" api_user="token_name" +enabled: true +entry_point: add_update_repository_team.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + org: + type: "string" + description: "The organization name. The name is not case sensitive." + required: true + team_slug: + type: "string" + description: "The slug of the team name." + required: true + owner: + type: "string" + description: "The account owner of the repository. The name is not case sensitive." + required: true + repo: + type: "string" + description: "The name of the repository. The name is not case sensitive." + required: true + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + permission: + type: "string" + description: "The permission to grant the team on this repository. In addition to the enumerated values, you can also specify a custom repository role name, if the owning organization has defined any. If no permission is specified, the team's permission attribute will be used to determine what permission to grant the team on this repository." + enum: + - "pull" + - "push" + - "admin" + - "maintain" + - "triage" + default: "pull" \ No newline at end of file diff --git a/actions/check_team_permissions_for_repository.py b/actions/check_team_permissions_for_repository.py new file mode 100644 index 0000000..baf127e --- /dev/null +++ b/actions/check_team_permissions_for_repository.py @@ -0,0 +1,35 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'CheckTeamPermissionsForRepository' +] + + +class CheckTeamPermissionsForRepository(BaseGithubAction): + def run(self, api_user, org, team_slug, owner, repo, github_type): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + try: + self._request("GET", + "/orgs/{}/teams/{}/repos/{}/{}".format(org, team_slug, owner, repo), + {}, + self.token, + enterprise) + + results = { + 'response': "The team {} has access to the repository {}".format(team_slug, repo) + } + except OSError as err: + raise err + except ValueError as err: + raise err + except Exception as err: + if str(err).find("404"): + results = {'response': "The team doesn't have access to the repository or was not found"} + else: + raise err + return results diff --git a/actions/check_team_permissions_for_repository.yaml b/actions/check_team_permissions_for_repository.yaml new file mode 100644 index 0000000..ce96781 --- /dev/null +++ b/actions/check_team_permissions_for_repository.yaml @@ -0,0 +1,37 @@ +--- +name: check_team_permissions_for_repository +runner_type: python-script +pack: github +description: > + Check if the given team has access to a repository. + Example: + st2 run github.check_team_permissions_for_repository org="organization" owner="owner" repo="reponame" team_slug="team_id" api_user="token_name" +enabled: true +entry_point: check_team_permissions_for_repository.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + org: + type: "string" + description: "The organization name. The name is not case sensitive." + required: true + team_slug: + type: "string" + description: "The slug of the team name." + required: true + owner: + type: "string" + description: "The account owner of the repository. The name is not case sensitive." + required: true + repo: + type: "string" + description: "The name of the repository. The name is not case sensitive." + required: true + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online \ No newline at end of file diff --git a/actions/check_user_repository_collaborator.py b/actions/check_user_repository_collaborator.py new file mode 100644 index 0000000..408d95d --- /dev/null +++ b/actions/check_user_repository_collaborator.py @@ -0,0 +1,32 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'CheckIfUserIsRepositoryCollaborator' +] + + +class CheckIfUserIsRepositoryCollaborator(BaseGithubAction): + def run(self, api_user, owner, repo, username, github_type): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + try: + self._request("GET", + "/repos/{}/{}/collaborators/{}".format(owner, repo, username), + {}, + self.token, + enterprise) + results = {'response': f"The user {username} is a Collaborator"} + except OSError as err: + raise err + except ValueError as err: + raise err + except Exception as err: + if str(err).find("404"): + results = {'response': f"The user {username} is not a Collaborator or not found"} + else: + raise err + return results diff --git a/actions/check_user_repository_collaborator.yaml b/actions/check_user_repository_collaborator.yaml new file mode 100644 index 0000000..32a505c --- /dev/null +++ b/actions/check_user_repository_collaborator.yaml @@ -0,0 +1,33 @@ +--- +name: check_user_repository_collaborator +runner_type: python-script +pack: github +description: > + Check if a user is a repository collaborator. + Example: + st2 run github.check_user_repository_collaborator owner="organization" repo="reponame" username="collaborator" api_user="token_name" +enabled: true +entry_point: check_user_repository_collaborator.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + owner: + type: "string" + description: "The account owner of the repository. The name is not case sensitive." + required: true + repo: + type: "string" + description: "The name of the repository. The name is not case sensitive." + required: true + username: + type: "string" + description: "The handle for the GitHub user account." + required: true + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online \ No newline at end of file diff --git a/actions/create_branch.py b/actions/create_branch.py new file mode 100644 index 0000000..cb6927a --- /dev/null +++ b/actions/create_branch.py @@ -0,0 +1,35 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'CreateBranchAction' +] + + +class CreateBranchAction(BaseGithubAction): + def run(self, api_user, new_branch, origin_ref, repository, github_type): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + # First, we have to get the sha1 for the given origin ref + response = self._request("GET", f"/repos/{repository}/git/ref/{origin_ref}", + {}, + self.token, + enterprise) + + if not response or not response['object']['sha']: + raise Exception(f"Could not get ref [{origin_ref}]. Response: {response}") + + # Then, we create the branch based on the origin ref + payload = {"ref": f"refs/heads/{new_branch}", + "sha": response['object']['sha']} + + response = self._request("POST", + f"/repos/{repository}/git/refs", + payload, + self.token, + enterprise) + + return {'response': response} diff --git a/actions/create_branch.yaml b/actions/create_branch.yaml new file mode 100644 index 0000000..03afd8f --- /dev/null +++ b/actions/create_branch.yaml @@ -0,0 +1,31 @@ +--- +name: "create_branch" +runner_type: "python-script" +description: > + Create a new branch for a GitHub repository + Example: + st2 run github.create_branch repository="owner/reponame" origin_ref="heads/" new_branch="branch_name" api_user="token_name" +enabled: true +entry_point: "create_branch.py" +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + repository: + type: "string" + description: "The full (Organization|User)/repository path" + required: true + origin_ref: + type: "string" + description: "The current reference to branch from (e.g. heads/master, heads/main)" + default: "heads/master" + new_branch: + type: "string" + description: "The branch to be created from the given ref" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/create_deployment.yaml b/actions/create_deployment.yaml index be6dcea..1d454e2 100644 --- a/actions/create_deployment.yaml +++ b/actions/create_deployment.yaml @@ -31,5 +31,7 @@ parameters: default: "" github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online \ No newline at end of file diff --git a/actions/create_deployment_status.yaml b/actions/create_deployment_status.yaml index 616b69c..31906ee 100644 --- a/actions/create_deployment_status.yaml +++ b/actions/create_deployment_status.yaml @@ -32,5 +32,7 @@ parameters: default: "" github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online \ No newline at end of file diff --git a/actions/create_file.py b/actions/create_file.py index d705fcb..a012328 100644 --- a/actions/create_file.py +++ b/actions/create_file.py @@ -2,39 +2,69 @@ from lib.formatters import file_response_to_dict, decode_base64 from lib.utils import prep_github_params_for_file_ops -__all__ = [ - 'CreateFileAction' -] +__all__ = ["CreateFileAction"] class CreateFileAction(BaseGithubAction): - def run(self, user, repo, path, message, content, branch=None, committer=None, author=None, - encoding=None): - author, branch, committer = prep_github_params_for_file_ops(author, branch, committer) + def run( + self, + user, + repo, + path, + message, + content, + github_type, + api_user, + branch=None, + committer=None, + author=None, + encoding=None, + ): + self._change_to_user_token_if_enterprise(api_user, github_type) + author, branch, committer = prep_github_params_for_file_ops( + author, branch, committer + ) - if encoding and encoding == 'base64': + if encoding and encoding == "base64": content = decode_base64(content) user = self._client.get_user(user) repo = user.get_repo(repo) - api_response = repo.create_file(path=path, message=message, content=content, branch=branch, - committer=committer, author=author) + api_response = repo.create_file( + path=path, + message=message, + content=content, + branch=branch, + committer=committer, + author=author, + ) result = file_response_to_dict(api_response) return result -if __name__ == '__main__': +if __name__ == "__main__": import os - GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') - GITHUB_ORG = os.environ.get('GITHUB_ORG') - GITHUB_REPO = os.environ.get('GITHUB_REPO') - COMMITTER = os.environ.get('COMMITTER', None) - AUTHOR = os.environ.get('AUTHOR', None) - - act = CreateFileAction(config={'token': GITHUB_TOKEN, 'github_type': 'online'}) - res = act.run(user=GITHUB_ORG, repo=GITHUB_REPO, path='README5.md', message='Test commit', - content='Super duper read me file, pushed from Stackstorm github pack!\n', - branch='branch1', committer=COMMITTER, author=AUTHOR) + + GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + GITHUB_ORG = os.environ.get("GITHUB_ORG") + GITHUB_REPO = os.environ.get("GITHUB_REPO") + COMMITTER = os.environ.get("COMMITTER", None) + AUTHOR = os.environ.get("AUTHOR", None) + + act = CreateFileAction(config={"token": GITHUB_TOKEN, "github_type": "online"}) + res = act.run( + user=GITHUB_ORG, + repo=GITHUB_REPO, + path="README5.md", + message="Test commit", + content="Super duper read me file, pushed from Stackstorm github pack!\n", + branch="branch1", + committer=COMMITTER, + author=AUTHOR, + github_type="online", + api_user="api_user", + ) import pprint + pp = pprint.PrettyPrinter(indent=4) pp.pprint(res) diff --git a/actions/create_file.yaml b/actions/create_file.yaml index bb5281c..bbb4a4a 100644 --- a/actions/create_file.yaml +++ b/actions/create_file.yaml @@ -45,3 +45,14 @@ parameters: type: "string" description: "If omitted this will be filled in with committer information. If passed, you must specify both a name and email. Expected format: FirstName LastName " required: false + + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online \ No newline at end of file diff --git a/actions/create_organization_repository.py b/actions/create_organization_repository.py new file mode 100644 index 0000000..bbcd898 --- /dev/null +++ b/actions/create_organization_repository.py @@ -0,0 +1,51 @@ +import time +import datetime + + +from lib.base import BaseGithubAction + +__all__ = [ + 'CreateOrganizationRepositoryAction' +] + + +class CreateOrganizationRepositoryAction(BaseGithubAction): + def run(self, api_user, org, name, description, github_type, homepage, private, visibility, + has_issues, has_projects, has_wiki, is_template, team_id, auto_init, + gitignore_template, license_template, allow_squash_merge, allow_merge_commit, + allow_rebase_merge, allow_auto_merge, delete_branch_on_merge): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + payload = {"name": name, + "description": description, + "homepage": homepage, + "private": private, + "visibility": visibility, + "has_issues": has_issues, + "has_projects": has_projects, + "has_wiki": has_wiki, + "is_template": is_template, + "team_id": team_id, + "auto_init": auto_init, + "gitignore_template": gitignore_template, + "license_template": license_template, + "allow_squash_merge": allow_squash_merge, + "allow_merge_commit": allow_merge_commit, + "allow_rebase_merge": allow_rebase_merge, + "allow_auto_merge": allow_auto_merge, + "delete_branch_on_merge": delete_branch_on_merge} + + response = self._request("POST", + "/orgs/{}/repos".format(org), + payload, + self.token, + enterprise) + + results = {'owner': response['owner']['login']} + results['response'] = response + + return results diff --git a/actions/create_organization_repository.yaml b/actions/create_organization_repository.yaml new file mode 100644 index 0000000..a40960a --- /dev/null +++ b/actions/create_organization_repository.yaml @@ -0,0 +1,87 @@ +--- +name: create_organization_repository +runner_type: python-script +pack: github +description: > + Creates a Github repository fot an organization. + Example: + st2 run github.create_organization_repository org="organization" name="reponame" description="test github.create_repository" private=true visibility="private" api_user="token_name" +enabled: true +entry_point: create_organization_repository.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + org: + type: "string" + description: "GitHub Organization." + required: true + name: + type: "string" + description: "The name of the repository." + required: true + description: + type: "string" + description: "A short description of the repository." + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + homepage: + type: "string" + description: "A URL with more information about the repository." + private: + type: "boolean" + description: "Whether the repository is private." + default: true + visibility: + type: "string" + description: "Can be public or private. If your organization is associated with an enterprise account using GitHub Enterprise Cloud or GitHub Enterprise Server 2.20+, visibility can also be internal. Note: For GitHub Enterprise Server and GitHub AE, this endpoint will only list repositories available to all users on the enterprise." + enum: + - "private" + - "public" + - "internal" + default: "private" + has_issues: + type: "boolean" + description: "Whether issues are enabled." + has_projects: + type: "boolean" + description: "Whether projects are enabled." + has_wiki: + type: "boolean" + description: "Whether the wiki is enabled." + is_template: + type: "boolean" + description: "Whether this repository acts as a template that can be used to generate new repositories." + default: false + team_id: + type: "integer" + description: "The id of the team that will be granted access to this repository. This is only valid when creating a repository in an organization." + auto_init: + type: "boolean" + description: "Whether the repository is initialized with a minimal README." + gitignore_template: + type: "string" + description: "The desired language or platform to apply to the .gitignore." + license_template: + type: "string" + description: "The license keyword of the open source license for this repository." + allow_squash_merge: + type: "boolean" + description: "Whether to allow squash merges for pull requests." + allow_merge_commit: + type: "boolean" + description: "Whether to allow merge commits for pull requests." + allow_rebase_merge: + type: "boolean" + description: "Whether to allow rebase merges for pull requests." + allow_auto_merge: + type: "boolean" + description: "Whether to allow Auto-merge to be used on pull requests." + delete_branch_on_merge: + type: "boolean" + description: "Whether to delete head branches when pull requests are merged" diff --git a/actions/create_pull.py b/actions/create_pull.py index 5274d95..965c508 100644 --- a/actions/create_pull.py +++ b/actions/create_pull.py @@ -7,7 +7,9 @@ class CreatePullAction(BaseGithubAction): - def run(self, user, repo, title, body, head, base): + def run(self, user, repo, title, body, head, base, api_user, github_type): + self._change_to_user_token_if_enterprise(api_user, github_type) + user = self._client.get_user(user) repo = user.get_repo(repo) pull = repo.create_pull(title=title, body=body, head=head, base=base) diff --git a/actions/create_pull.yaml b/actions/create_pull.yaml index d1caf79..a8ea3d1 100644 --- a/actions/create_pull.yaml +++ b/actions/create_pull.yaml @@ -32,3 +32,14 @@ parameters: type: "string" description: "The name of the branch you want the changes pulled into. This should be an existing branch on the current repository. You cannot submit a pull request to one repository that requests a merge to a base of another repository." required: true + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + diff --git a/actions/create_release.yaml b/actions/create_release.yaml index 7142b59..b9042e6 100644 --- a/actions/create_release.yaml +++ b/actions/create_release.yaml @@ -45,5 +45,7 @@ parameters: immutable: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/create_repository_authenticated_user.py b/actions/create_repository_authenticated_user.py new file mode 100644 index 0000000..66c9da0 --- /dev/null +++ b/actions/create_repository_authenticated_user.py @@ -0,0 +1,47 @@ + +from lib.base import BaseGithubAction + +__all__ = [ + 'CreateRepositoryAuthenticatedUserAction' +] + +class CreateRepositoryAuthenticatedUserAction(BaseGithubAction): + def run(self, api_user, name, description, github_type, homepage, private, + has_issues, has_projects, has_wiki, team_id, auto_init, gitignore_template, + license_template, allow_squash_merge, allow_merge_commit, allow_rebase_merge, + allow_auto_merge, delete_branch_on_merge, has_downloads, is_template, ): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + payload = { "name": name, + "description": description, + "homepage": homepage, + "private": private, + "has_issues": has_issues, + "has_projects": has_projects, + "has_wiki": has_wiki, + "team_id": team_id, + "auto_init": auto_init, + "gitignore_template": gitignore_template, + "license_template": license_template, + "allow_squash_merge": allow_squash_merge, + "allow_merge_commit": allow_merge_commit, + "allow_rebase_merge": allow_rebase_merge, + "allow_auto_merge": allow_auto_merge, + "delete_branch_on_merge": delete_branch_on_merge, + "has_downloads": has_downloads, + "is_template": is_template} + + response = self._request("POST", + "/user/repos", + payload, + self.token, + enterprise) + + results = {'owner': response['owner']['login']} + results['response'] = response + + return results diff --git a/actions/create_repository_authenticated_user.yaml b/actions/create_repository_authenticated_user.yaml new file mode 100644 index 0000000..20189a8 --- /dev/null +++ b/actions/create_repository_authenticated_user.yaml @@ -0,0 +1,78 @@ +--- +name: create_repository_authenticated_user +runner_type: python-script +pack: github +description: > + Creates a Github repository for the authenticated user. + Example: + st2 run github.create_repository_authenticated_user name="reponame" description="test github.create_repository" private=false api_user="token_name" +enabled: true +entry_point: create_repository_authenticated_user.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + name: + type: "string" + description: "The name of the repository." + required: true + description: + type: "string" + description: "A short description of the repository." + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + homepage: + type: "string" + description: "A URL with more information about the repository." + private: + type: "boolean" + description: "Whether the repository is private." + default: true + has_issues: + type: "boolean" + description: "Whether issues are enabled." + has_projects: + type: "boolean" + description: "Whether projects are enabled." + has_wiki: + type: "boolean" + description: "Whether the wiki is enabled." + team_id: + type: "integer" + description: "The id of the team that will be granted access to this repository. This is only valid when creating a repository in an organization." + auto_init: + type: "boolean" + description: "Whether the repository is initialized with a minimal README." + gitignore_template: + type: "string" + description: "The desired language or platform to apply to the .gitignore." + license_template: + type: "string" + description: "The license keyword of the open source license for this repository." + allow_squash_merge: + type: "boolean" + description: "Whether to allow squash merges for pull requests." + allow_merge_commit: + type: "boolean" + description: "Whether to allow merge commits for pull requests." + allow_rebase_merge: + type: "boolean" + description: "Whether to allow rebase merges for pull requests." + allow_auto_merge: + type: "boolean" + description: "Whether to allow Auto-merge to be used on pull requests." + delete_branch_on_merge: + type: "boolean" + description: "Whether to delete head branches when pull requests are merged" + has_downloads: + type: "boolean" + description: "Whether downloads are enabled." + is_template: + type: "boolean" + description: "Whether this repository acts as a template that can be used to generate new repositories." + default: false diff --git a/actions/create_repository_from_template.py b/actions/create_repository_from_template.py new file mode 100644 index 0000000..c056e7d --- /dev/null +++ b/actions/create_repository_from_template.py @@ -0,0 +1,35 @@ +import time +import datetime + + +from lib.base import BaseGithubAction + +__all__ = [ + 'CreateRepositoryFromTemplateAction' +] + +class CreateRepositoryFromTemplateAction(BaseGithubAction): + def run(self, api_user, github_type, template_owner,template_repo, + owner, name, description, include_all_branches, private): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + payload = { "owner": owner, + "name": name, + "description": description, + "include_all_branches": include_all_branches, + "private": private} + + response = self._request("POST", + "/repos/{}/{}/generate".format(template_owner, template_repo), + payload, + self.token, + enterprise) + + results = {'owner': response['owner']['login']} + results['response'] = response + + return results diff --git a/actions/create_repository_from_template.yaml b/actions/create_repository_from_template.yaml new file mode 100644 index 0000000..afaeeb8 --- /dev/null +++ b/actions/create_repository_from_template.yaml @@ -0,0 +1,46 @@ +--- +name: create_repository_from_template +runner_type: python-script +pack: github +description: > + Creates a Github repository from a template repository + Example: + st2 run github.create_repository_from_template owner="organization" name="reponame" description="test github.create_repository" private=true template_owner="gittemplate" template_repo="gitrepo" api_user="token_name" +enabled: true +entry_point: create_repository_from_template.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online + template_owner: + type: "string" + description: "The template owner." + template_repo: + type: "string" + description: "The template repository." + owner: + type: "string" + description: "The organization or person who will own the new repository. To create a new repository in an organization, the authenticated user must be a member of the specified organization." + required: true + name: + type: "string" + description: "The name of the repository." + required: true + description: + type: "string" + description: "A short description of the repository." + include_all_branches: + type: "boolean" + description: "Set to true to include the directory structure and files from all branches in the template repository, and not just the default branch. Default: false." + default: false + private: + type: "boolean" + description: "Either true to create a new private repository or false to create a new public one." + default: true diff --git a/actions/delete_branch.py b/actions/delete_branch.py new file mode 100644 index 0000000..a120483 --- /dev/null +++ b/actions/delete_branch.py @@ -0,0 +1,22 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'DeleteBranchAction' +] + + +class DeleteBranchAction(BaseGithubAction): + def run(self, api_user, branch, repository, github_type): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + response = self._request("DELETE", + f"/repos/{repository}/git/refs/heads/{branch}", + {}, + self.token, + enterprise) + + return {'response': response} diff --git a/actions/delete_branch.yaml b/actions/delete_branch.yaml new file mode 100644 index 0000000..b89082f --- /dev/null +++ b/actions/delete_branch.yaml @@ -0,0 +1,27 @@ +--- +name: "delete_branch" +runner_type: "python-script" +description: > + Deletes a branch from a GitHub repository + Example: + st2 run github.delete_branch repository="owner/reponame" branch="branch_name" api_user="token_name" +enabled: true +entry_point: "delete_branch.py" +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + repository: + type: "string" + description: "The full (Organization|User)/repository path" + required: true + branch: + type: "string" + description: "The branch to be deleted from the given ref" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/get_branch.py b/actions/get_branch.py new file mode 100644 index 0000000..e1e1623 --- /dev/null +++ b/actions/get_branch.py @@ -0,0 +1,22 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'GetBranchAction' +] + + +class GetBranchAction(BaseGithubAction): + def run(self, api_user, branch, repository, github_type): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + # First, we have to get the sha1 for the given origin ref + response = self._request("GET", f"/repos/{repository}/git/ref/heads/{branch}", + {}, + self.token, + enterprise) + + return {'response': response} diff --git a/actions/get_branch.yaml b/actions/get_branch.yaml new file mode 100644 index 0000000..5c1135f --- /dev/null +++ b/actions/get_branch.yaml @@ -0,0 +1,27 @@ +--- +name: "get_branch" +runner_type: "python-script" +description: > + Gets branch details from a GitHub repository + Example: + st2 run github.get_branch repository="owner/reponame" branch="branch_name" api_user="token_name" +enabled: true +entry_point: "get_branch.py" +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + repository: + type: "string" + description: "The full (Organization|User)/repository path" + required: true + branch: + type: "string" + description: "The name of the branch to fetch details for" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/get_clone_stats.py b/actions/get_clone_stats.py index 0d235af..8e3befc 100644 --- a/actions/get_clone_stats.py +++ b/actions/get_clone_stats.py @@ -8,5 +8,6 @@ class GetCloneStatsAction(BaseGithubAction): def run(self, repo, github_type): clone_data = self._get_analytics( - category='clone-activity-data', repo=repo, enterprise=self._is_enterprise(github_type)) + category='clone-activity-data', repo=repo, + enterprise=self._is_enterprise(github_type)) return clone_data['summary'] diff --git a/actions/get_clone_stats.yaml b/actions/get_clone_stats.yaml index 6b684c5..6b66161 100644 --- a/actions/get_clone_stats.yaml +++ b/actions/get_clone_stats.yaml @@ -11,5 +11,7 @@ parameters: required: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/get_deployment_statuses.yaml b/actions/get_deployment_statuses.yaml index 127bc27..3950a1f 100644 --- a/actions/get_deployment_statuses.yaml +++ b/actions/get_deployment_statuses.yaml @@ -19,5 +19,7 @@ parameters: required: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/get_repository_collaborators.py b/actions/get_repository_collaborators.py new file mode 100644 index 0000000..649af36 --- /dev/null +++ b/actions/get_repository_collaborators.py @@ -0,0 +1,29 @@ +from lib.base import BaseGithubAction + +__all__ = [ + 'GetRepositoryCollaborators' +] + + +class GetRepositoryCollaborators(BaseGithubAction): + def run(self, api_user, owner, repo, affiliation, + per_page, page, github_type): + + enterprise = self._is_enterprise(github_type) + + if api_user: + self.token = self._get_user_token(api_user, enterprise) + + payload = {"affiliation": affiliation, + "per_page": per_page, + "page": page} + + response = self._request("GET", + "/repos/{}/{}/collaborators".format(owner, repo), + payload, + self.token, + enterprise) + + results = {'response': response} + + return results diff --git a/actions/get_repository_collaborators.yaml b/actions/get_repository_collaborators.yaml new file mode 100644 index 0000000..f8f2729 --- /dev/null +++ b/actions/get_repository_collaborators.yaml @@ -0,0 +1,45 @@ +--- +name: get_repository_collaborators +runner_type: python-script +pack: github +description: > + List repository collaborators. + Example: + st2 run github.get_repository_collaborators owner="organization" repo="reponame" api_user="token_name" +enabled: true +entry_point: get_repository_collaborators.py +parameters: + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + owner: + type: "string" + description: "The account owner of the repository. The name is not case sensitive." + required: true + repo: + type: "string" + description: "The name of the repository. The name is not case sensitive." + required: true + affiliation: + type: "string" + description: "Filter collaborators returned by their affiliation. outside means all outside collaborators of an organization-owned repository. direct means all collaborators with permissions to an organization-owned repository, regardless of organization membership status. all means all collaborators the authenticated user can see." + enum: + - "outside" + - "direct" + - "all" + default: "all" + per_page: + type: "integer" + description: "The number of results per page (max 100)." + default: 30 + page: + type: "integer" + description: "Page number of the results to fetch." + default: 1 + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online \ No newline at end of file diff --git a/actions/get_traffic_stats.py b/actions/get_traffic_stats.py index 0a76e42..e14378b 100644 --- a/actions/get_traffic_stats.py +++ b/actions/get_traffic_stats.py @@ -8,5 +8,6 @@ class GetTrafficStatsAction(BaseGithubAction): def run(self, repo, github_type): traffic_data = self._get_analytics( - category='traffic-data', repo=repo, enterprise=self._is_enterprise(github_type)) + category='traffic-data', repo=repo, + enterprise=self._is_enterprise(github_type)) return traffic_data['summary'] diff --git a/actions/get_traffic_stats.yaml b/actions/get_traffic_stats.yaml index a091ba7..3b42c2d 100644 --- a/actions/get_traffic_stats.yaml +++ b/actions/get_traffic_stats.yaml @@ -11,5 +11,7 @@ parameters: required: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/get_user.py b/actions/get_user.py index 9b25dd0..1a50a51 100644 --- a/actions/get_user.py +++ b/actions/get_user.py @@ -8,9 +8,7 @@ class GetUserAction(BaseGithubAction): def run(self, user, token_user, github_type): - enterprise = self._is_enterprise(github_type) - if token_user: - self._change_to_user_token(token_user, enterprise) + self._change_to_user_token_if_enterprise(token_user, github_type) user = self._client.get_user(user) result = user_to_dict(user=user) diff --git a/actions/get_user.yaml b/actions/get_user.yaml index acb15f7..86b9b7e 100644 --- a/actions/get_user.yaml +++ b/actions/get_user.yaml @@ -11,9 +11,11 @@ parameters: required: true token_user: type: "string" - description: "The" + description: "The API user" default: "{{action_context.api_user|default(None)}}" github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/latest_release.yaml b/actions/latest_release.yaml index bfa8608..daf2f62 100644 --- a/actions/latest_release.yaml +++ b/actions/latest_release.yaml @@ -15,5 +15,7 @@ parameters: required: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/lib/base.py b/actions/lib/base.py index 9b5a588..9169096 100644 --- a/actions/lib/base.py +++ b/actions/lib/base.py @@ -2,6 +2,7 @@ import requests from bs4 import BeautifulSoup import json +import logging from st2common.runners.base_action import Action @@ -18,6 +19,7 @@ class BaseGithubAction(Action): + def run(self, **kwargs): pass @@ -70,6 +72,8 @@ def _get_analytics(self, category, repo, enterprise): response = s.get(url) return response.json() + # Whether or not this execution is meant for enterprise github installation (on-premises) + # or online installations (in the cloud) def _is_enterprise(self, github_type): if github_type == "enterprise": @@ -83,6 +87,8 @@ def _is_enterprise(self, github_type): else: raise ValueError("Default GitHub Invalid!") + # Github token will come from KV using this function.. and depending on whether + # it's for enterprise or not, it will return have either of the key prefix below def _get_user_token(self, user, enterprise): """ Return a users GitHub OAuth Token, if it fails replace '-' @@ -94,7 +100,7 @@ def _get_user_token(self, user, enterprise): else: token_name = "token_" - token = self.action_service.get_value(token_name + user) + token = self.action_service.get_value(token_name + user, local=False, decrypt=True) # if a token is not returned, try using reversing changes made by # GitHub Enterprise during LDAP sync'ing. @@ -104,7 +110,15 @@ def _get_user_token(self, user, enterprise): return token + def _change_to_user_token_if_enterprise(self, api_user, github_type): + enterprise = self._is_enterprise(github_type) + if api_user: + self._change_to_user_token(api_user, enterprise) + + # Changes the internal client used on this instance of action execution to + # the one matching the configuration for enterprise/online and user given here def _change_to_user_token(self, user, enterprise): + logging.debug("Changing github client for user [%s] and enterprise [%s]", user, enterprise) token = self._get_user_token(user, enterprise) if enterprise: @@ -114,6 +128,7 @@ def _change_to_user_token(self, user, enterprise): return True + # Sends a generic HTTP/s request to the github endpoint def _request(self, method, uri, payload, token, enterprise): headers = {'Authorization': 'token {}'.format(token)} @@ -132,8 +147,8 @@ def _request(self, method, uri, payload, token, enterprise): r.raise_for_status() except requests.exceptions.HTTPError: raise Exception( - "ERROR: '{}'ing to '{}' - status code: {} payload: {}".format( - method, url, r.status_code, json.dumps(payload))) + "ERROR: '{}'ing to '{}' - status code: {} payload: {} response: {}".format( + method, url, r.status_code, json.dumps(payload), r.json())) except requests.exceptions.ConnectionError as e: raise Exception("Could not connect to: {} : {}".format(url, e)) else: diff --git a/actions/list_deployments.yaml b/actions/list_deployments.yaml index 93dbfc1..e89e9b9 100644 --- a/actions/list_deployments.yaml +++ b/actions/list_deployments.yaml @@ -15,5 +15,7 @@ parameters: required: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/list_releases.yaml b/actions/list_releases.yaml index 950c78d..824aa1f 100644 --- a/actions/list_releases.yaml +++ b/actions/list_releases.yaml @@ -15,5 +15,7 @@ parameters: required: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/store_oauth_token.py b/actions/store_oauth_token.py index ac0429c..3599d97 100644 --- a/actions/store_oauth_token.py +++ b/actions/store_oauth_token.py @@ -28,6 +28,8 @@ def run(self, user, token, github_type): self.action_service.set_value( name=value_name, + local=False, + encrypt=True, value=token.strip()) return results diff --git a/actions/store_oauth_token.yaml b/actions/store_oauth_token.yaml index 8bed971..b4bb8bb 100644 --- a/actions/store_oauth_token.yaml +++ b/actions/store_oauth_token.yaml @@ -13,7 +13,10 @@ parameters: type: "string" description: "The GitHub OAuth token" required: true + secret: true github_type: type: "string" - description: "The type of github installation to target, if unset will use the configured default." - default: ~ + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/actions/update_branch_protection.py b/actions/update_branch_protection.py index e20cc96..5f9f8ad 100644 --- a/actions/update_branch_protection.py +++ b/actions/update_branch_protection.py @@ -1,15 +1,20 @@ from lib.base import BaseGithubAction from github.GithubObject import NotSet -__all__ = [ - 'UpdateBranchProtectionAction' -] +__all__ = ["UpdateBranchProtectionAction"] class UpdateBranchProtectionAction(BaseGithubAction): - def run(self, user, repo, branch, required_status_checks, enforce_admins, - required_pull_request_reviews, restrictions, required_linear_history=False, - allow_force_pushes=False, allow_deletions=False): + def run( + self, + user, + repo, + branch, + required_status_checks, + enforce_admins, + required_pull_request_reviews, + restrictions, + ): user = self._client.get_user(user) repo = user.get_repo(repo) @@ -19,8 +24,8 @@ def run(self, user, repo, branch, required_status_checks, enforce_admins, strict = NotSet contexts = NotSet else: - strict = required_status_checks['strict'] - contexts = required_status_checks['contexts'] + strict = required_status_checks["strict"] + contexts = required_status_checks["contexts"] if not required_pull_request_reviews: dismissal_users = NotSet @@ -29,19 +34,24 @@ def run(self, user, repo, branch, required_status_checks, enforce_admins, require_code_owner_reviews = NotSet required_approving_review_count = NotSet else: - dismissal_users = required_pull_request_reviews['dismissal_users'] - dismissal_teams = required_pull_request_reviews['dismissal_teams'] - dismiss_stale_reviews = required_pull_request_reviews['dismiss_stale_reviews'] - require_code_owner_reviews = required_pull_request_reviews['require_code_owner_reviews'] + dismissal_users = required_pull_request_reviews["dismissal_users"] + dismissal_teams = required_pull_request_reviews["dismissal_teams"] + dismiss_stale_reviews = required_pull_request_reviews[ + "dismiss_stale_reviews" + ] + require_code_owner_reviews = required_pull_request_reviews[ + "require_code_owner_reviews" + ] required_approving_review_count = required_pull_request_reviews[ - 'required_approving_review_count'] + "required_approving_review_count" + ] if not restrictions: user_push_restrictions = NotSet team_push_restrictions = NotSet else: - user_push_restrictions = restrictions['user_push_restrictions'] - team_push_restrictions = restrictions['team_push_restrictions'] + user_push_restrictions = restrictions["user_push_restrictions"] + team_push_restrictions = restrictions["team_push_restrictions"] if not dismissal_users: dismissal_users = NotSet @@ -60,13 +70,13 @@ def run(self, user, repo, branch, required_status_checks, enforce_admins, return True -if __name__ == '__main__': +if __name__ == "__main__": import os - GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') - GITHUB_ORG = os.environ.get('GITHUB_ORG') - GITHUB_REPO = os.environ.get('GITHUB_REPO') - GITHUB_BRANCH = os.environ.get('GITHUB_BRANCH') + GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + GITHUB_ORG = os.environ.get("GITHUB_ORG") + GITHUB_REPO = os.environ.get("GITHUB_REPO") + GITHUB_BRANCH = os.environ.get("GITHUB_BRANCH") # As produced by get_branch_protection action BRANCH_PROTECTION = {'enforce_admins': False, @@ -79,12 +89,20 @@ def run(self, user, repo, branch, required_status_checks, enforce_admins, 'restrictions': None } - act = UpdateBranchProtectionAction(config={'token': GITHUB_TOKEN, 'github_type': 'online'}) - res = act.run(user=GITHUB_ORG, repo=GITHUB_REPO, branch=GITHUB_BRANCH, - required_status_checks=BRANCH_PROTECTION['required_status_checks'], - enforce_admins=BRANCH_PROTECTION['enforce_admins'], - required_pull_request_reviews=BRANCH_PROTECTION['required_pull_request_reviews'], - restrictions=BRANCH_PROTECTION['restrictions']) + act = UpdateBranchProtectionAction( + config={"token": GITHUB_TOKEN, "github_type": "online"} + ) + res = act.run( + user=GITHUB_ORG, + repo=GITHUB_REPO, + branch=GITHUB_BRANCH, + required_status_checks=BRANCH_PROTECTION["required_status_checks"], + enforce_admins=BRANCH_PROTECTION["enforce_admins"], + required_pull_request_reviews=BRANCH_PROTECTION[ + "required_pull_request_reviews" + ], + restrictions=BRANCH_PROTECTION["restrictions"], + ) import pprint pp = pprint.PrettyPrinter(indent=4) diff --git a/actions/update_file.py b/actions/update_file.py index b36fc45..57ef4fe 100644 --- a/actions/update_file.py +++ b/actions/update_file.py @@ -2,43 +2,72 @@ from lib.formatters import file_response_to_dict, decode_base64 from lib.utils import prep_github_params_for_file_ops -__all__ = [ - 'UpdateFileAction' -] +__all__ = ["UpdateFileAction"] class UpdateFileAction(BaseGithubAction): - def run(self, user, repo, path, message, content, sha, branch=None, committer=None, - author=None, encoding=None): - author, branch, committer = prep_github_params_for_file_ops(author, branch, committer) + def run( + self, + user, + repo, + path, + message, + content, + sha, + api_user, + github_type, + branch=None, + committer=None, + author=None, + encoding=None, + ): + self._change_to_user_token_if_enterprise(api_user, github_type) + author, branch, committer = prep_github_params_for_file_ops( + author, branch, committer + ) - if encoding and encoding == 'base64': + if encoding and encoding == "base64": content = decode_base64(content) user = self._client.get_user(user) repo = user.get_repo(repo) - api_response = repo.update_file(path=path, message=message, content=content, sha=sha, - branch=branch, committer=committer, author=author) + api_response = repo.update_file( + path=path, + message=message, + content=content, + sha=sha, + branch=branch, + committer=committer, + author=author, + ) result = file_response_to_dict(api_response) return result -if __name__ == '__main__': +if __name__ == "__main__": import os - GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN') - GITHUB_ORG = os.environ.get('GITHUB_ORG') - GITHUB_REPO = os.environ.get('GITHUB_REPO') - COMMITTER = os.environ.get('COMMITTER', None) - AUTHOR = os.environ.get('AUTHOR', None) - - act = UpdateFileAction(config={'token': GITHUB_TOKEN, 'github_type': 'online'}) - res = act.run(user=GITHUB_ORG, repo=GITHUB_REPO, path='README.md', - message='Test commit, committer: {}, author: {}'.format(COMMITTER, AUTHOR), - content='Super duper read me file, pushed from Stackstorm github pack!\n' - '##new lines added!\n\n*YES*\nHOORAY!!!\nHELL YEAH!\n', - sha='058d97c135546cf5d1a029dabc16c22313a8c90b', - branch='branch1', committer=COMMITTER, author=AUTHOR) + GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + GITHUB_ORG = os.environ.get("GITHUB_ORG") + GITHUB_REPO = os.environ.get("GITHUB_REPO") + COMMITTER = os.environ.get("COMMITTER", None) + AUTHOR = os.environ.get("AUTHOR", None) + + act = UpdateFileAction(config={"token": GITHUB_TOKEN, "github_type": "online"}) + res = act.run( + user=GITHUB_ORG, + repo=GITHUB_REPO, + path="README.md", + message="Test commit, committer: {}, author: {}".format(COMMITTER, AUTHOR), + content="Super duper read me file, pushed from Stackstorm github pack!\n" + "##new lines added!\n\n*YES*\nHOORAY!!!\nHELL YEAH!\n", + sha="058d97c135546cf5d1a029dabc16c22313a8c90b", + branch="branch1", + committer=COMMITTER, + author=AUTHOR, + github_type="online", + api_user="api_user", + ) import pprint pp = pprint.PrettyPrinter(indent=4) diff --git a/actions/update_file.yaml b/actions/update_file.yaml index bdbaa62..2835b20 100644 --- a/actions/update_file.yaml +++ b/actions/update_file.yaml @@ -49,3 +49,13 @@ parameters: type: "string" description: "If omitted this will be filled in with committer information. If passed, you must specify both a name and email. Expected format: FirstName LastName " required: false + api_user: + type: "string" + description: "The API user" + default: "{{action_context.api_user|default(None)}}" + github_type: + type: "string" + description: "The type of github API to target, if unset will use the configured pack default. Enterprise means self-hosted API, e.g. github.your-company.com. Online means api.github.com" + enum: + - enterprise + - online diff --git a/pack.yaml b/pack.yaml index 3c5fd56..b991709 100644 --- a/pack.yaml +++ b/pack.yaml @@ -8,7 +8,7 @@ keywords: - git - scm - serverless -version: 2.1.3 +version: 2.2.0 python_versions: - "3" author : StackStorm, Inc. diff --git a/tests/fixtures/add_update_repository_environment/result_successful.json b/tests/fixtures/add_update_repository_environment/result_successful.json new file mode 100644 index 0000000..879c49d --- /dev/null +++ b/tests/fixtures/add_update_repository_environment/result_successful.json @@ -0,0 +1,73 @@ +{ + "id": 123, + "node_id": "MDExOkVudmlyb25tZW50MTYxMDg4MDY4", + "name": "staging", + "url": "https://api.github.com/repos/github/hello-world/environments/staging", + "html_url": "https://github.com/github/hello-world/deployments/activity_log?environments_filter=staging", + "created_at": "2020-11-23T22:00:40Z", + "updated_at": "2020-11-23T22:00:40Z", + "protection_rules": [ + { + "id": 33, + "node_id": "MDQ6R2F0ZTM3MzY=", + "type": "wait_timer", + "wait_timer": 30 + }, + { + "id": 44, + "node_id": "MDQ6R2F0ZTM3NTU=", + "type": "required_reviewers", + "reviewers": [ + { + "type": "User", + "reviewer": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + } + }, + { + "type": "Team", + "reviewer": { + "id": 1, + "node_id": "MDQ6VGVhbTE=", + "url": "https://api.github.com/teams/1", + "html_url": "https://github.com/orgs/github/teams/justice-league", + "name": "Justice League", + "slug": "justice-league", + "description": "A great team.", + "privacy": "closed", + "permission": "admin", + "members_url": "https://api.github.com/teams/1/members{/member}", + "repositories_url": "https://api.github.com/teams/1/repos", + "parent": null + } + } + ] + }, + { + "id": 55, + "node_id": "MDQ6R2F0ZTM3NTY=", + "type": "branch_policy" + } + ], + "deployment_branch_policy": { + "protected_branches": false, + "custom_branch_policies": true + } + } \ No newline at end of file diff --git a/tests/github_base_action_test_case.py b/tests/github_base_action_test_case.py index e33c9c3..f580b4c 100644 --- a/tests/github_base_action_test_case.py +++ b/tests/github_base_action_test_case.py @@ -13,7 +13,9 @@ # See the License for the specific language governing permissions and import yaml +import json # from mock import MagicMock +from lib.base import BaseGithubAction from st2tests.base import BaseActionTestCase @@ -21,6 +23,17 @@ class GitHubBaseActionTestCase(BaseActionTestCase): __test__ = False + + + + def _mock_request(self, method, uri, data, *args, **kwargs): + # Defaults to using old request :) + return self.oldRequest(method, uri, data, *args, **kwargs) + + def tearDown(self): + super(GitHubBaseActionTestCase, self).tearDown() + BaseGithubAction._request = self.oldRequest + def setUp(self): super(GitHubBaseActionTestCase, self).setUp() @@ -29,8 +42,14 @@ def setUp(self): self._enterprise_default_config = self.load_yaml( 'full-enterprise.yaml') + self.oldRequest = BaseGithubAction._request + BaseGithubAction._request = self._mock_request + + def load_yaml(self, filename): return yaml.safe_load(self.get_fixture_content(filename)) + def load_json(self, filename): + return json.loads(self.get_fixture_content(filename)) @property def blank_config(self): diff --git a/tests/test_action_add_update_repository_environment.py b/tests/test_action_add_update_repository_environment.py new file mode 100644 index 0000000..c314652 --- /dev/null +++ b/tests/test_action_add_update_repository_environment.py @@ -0,0 +1,119 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and + +# from mock import MagicMock + +from github_base_action_test_case import GitHubBaseActionTestCase +import mock +import requests +import json + + +from add_update_repository_environment import AddUpdateRepositoryEnvironmentAction + + +class AddUpdateRepositoryEnvironmentActionTestCase(GitHubBaseActionTestCase): + __test__ = True + action_cls = AddUpdateRepositoryEnvironmentAction + + expectedCreateEnvPayload = None + + def _mock_request(self, method, uri, data, *args, **kwargs): + if uri == "/repos/org/repo/environments/env-test": + self.assertEquals(data, self.expectedCreateEnvPayload) + return self.load_json('add_update_repository_environment/result_successful.json') + + if uri == "/orgs/org/teams/test-team": + self.assertEquals(data, None) + return { + 'id': 123 + } + + return super()._mock_request(method, uri, data, *args, **kwargs) + + def test_successful(self): + + action = self.get_action_instance(self.full_config) + self.assertIsInstance(action, self.action_cls) + + expected_results = { + 'response': self.load_json('add_update_repository_environment/result_successful.json') + } + self.expectedCreateEnvPayload = ( + { + "wait_timer": 0, + "reviewers": [ + { + "type": "Team", + "id": 123 + } + ], + "deployment_branch_policy": None + }) + + results = action.run( + api_user="test", + environment="env-test", + owner="org", + repo="repo", + github_type="online", + reviewers=[ + { + "type": "Team", + "id": 123 + } + ], + wait_timer=0, + deployment_branch_policy=None + ) + + self.assertEquals(results, expected_results) + + def test_successful_team_with_name(self): + + action = self.get_action_instance(self.full_config) + self.assertIsInstance(action, self.action_cls) + + expected_results = { + 'response': self.load_json('add_update_repository_environment/result_successful.json') + } + self.expectedCreateEnvPayload = ( + { + "wait_timer": 0, + "reviewers": [ + { + "type": "Team", + "id": 123 + } + ], + "deployment_branch_policy": None + }) + + results = action.run( + api_user="test", + environment="env-test", + owner="org", + repo="repo", + github_type="online", + reviewers=[ + { + "type": "Team", + "name": "test-team" + } + ], + wait_timer=0, + deployment_branch_policy=None + ) + + self.assertEquals(results, expected_results)