From 16625a986691fc54185ec5cb4e21207f4997edbc Mon Sep 17 00:00:00 2001 From: Noah Canadea Date: Sun, 29 Oct 2023 14:08:29 +0100 Subject: [PATCH] test action (#2) * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action * test action --- .github/workflows/action_integration_test.yml | 29 +++++++++++++- action.yml | 38 ++++++++++++------ infrapatch/action/__main__.py | 40 +++++++++++++++++-- infrapatch/cli/cli.py | 11 +++-- infrapatch/core/composition.py | 28 +++++++------ infrapatch/core/constants.py | 2 +- requirements.txt | 3 +- 7 files changed, 113 insertions(+), 38 deletions(-) diff --git a/.github/workflows/action_integration_test.yml b/.github/workflows/action_integration_test.yml index 162264a..8228a1e 100644 --- a/.github/workflows/action_integration_test.yml +++ b/.github/workflows/action_integration_test.yml @@ -12,6 +12,9 @@ on: jobs: integration-test: + env: + report_json_file: InfraPatch_Statistics.json + name: "Run GitHub Action integration test" runs-on: ubuntu-latest steps: @@ -21,14 +24,36 @@ jobs: - name: Run in report only mode uses: ./ with: - push_changes: false report_only: true - name: Run in update mode + id: update uses: ./ with: - push_changes: false report_only: false + target_branch_name: "feat/infrapatch_test_${{ github.run_number }}" + + - name: Check update result + shell: pwsh + run: | + $report = Get-Content $env:report_json_file -Raw | ConvertFrom-Json + if ( -not $report.total_resources -gt 0 ) { + throw "Failed to get resources" + } + if ( -not ( $report.resources_patched -gt 3 ) ) { + throw "No resources should be patched" + } + if ( $report.errors -gt 0 ) { + throw "Errors have been detected" + } + + - name: Delete created branch$ + if: always() + uses: dawidd6/action-delete-branch@v3 + with: + github_token: ${{github.token}} + branches: ${{ steps.update.outputs.target_branch }} + soft_fail: true diff --git a/action.yml b/action.yml index 103162d..f43adb2 100644 --- a/action.yml +++ b/action.yml @@ -6,6 +6,10 @@ inputs: description: "Name of the branch where changes will be pushed to. Defaults to feature/infrapatch-bot" required: true default: "feature/infrapatch-bot" + repository_name: + description: "Name of the repository to run the action in. Defaults to the current repository" + required: false + default: ${{ github.repository }} default_registry_domain: description: "Default registry domain to use for modules and providers without explicit registry domain set. Defaults to registry.terraform.io" required: false @@ -33,16 +37,27 @@ inputs: description: "Working directory to run the command in. Defaults to the root of the repository" required: false default: ${{ github.workspace }} +outputs: + target_branch: + description: "Name of the branch where changes will be pushed to" + value: ${{ inputs.target_branch_name }} + runs: using: composite steps: - name: Extract branch name - id: head_branch + id: branch shell: bash run: | - head_branch="origin/${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" - echo "Detected head branch: $head_branch" - echo "branch=$head_branch" >> $GITHUB_OUTPUT + head_branch_origin="origin/${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" + head_branch="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" + target_branch="${{ inputs.target_branch_name }}" + target_branch_origin="origin/${{ inputs.target_branch_name }}" + echo "Using head branch $head_branch and target branch $target_branch" + echo "head=$head_branch" >> $GITHUB_OUTPUT + echo "head_origin=$head_branch_origin" >> $GITHUB_OUTPUT + echo "target=$target_branch" >> $GITHUB_OUTPUT + echo "target_origin=$target_branch_origin" >> $GITHUB_OUTPUT - name: Install Python uses: actions/setup-python@v4 @@ -78,15 +93,15 @@ runs: shell: bash run: | git fetch origin - git checkout -b "${{ inputs.target_branch_name }}" "origin/${{ inputs.target_branch_name }}" + git checkout -b "${{ steps.branch.outputs.target }}" "${{ steps.branch.outputs.target_origin }}" - name: Rebase target branch if: ${{ steps.create_branch.outputs.created == 'false' }} working-directory: ${{ inputs.working_directory }} shell: bash run: | - echo "Rebasing ${{ inputs.target_branch_name }} on ${{ steps.head_branch.outputs.branch }}" - git rebase -Xtheirs ${{ steps.head_branch.outputs.branch }} + echo "Rebasing ${{ steps.branch.outputs.target }} on ${{ steps.branch.outputs.head_origin }}" + git rebase -Xtheirs ${{ steps.branch.outputs.head_origin }} - name: Run InfraPatch Action shell: bash @@ -103,13 +118,10 @@ runs: arguments+=("--registry-secrets-string" "\"${{ inputs.registry_secrets }}\"") fi arguments+=("--github-token" "${{ inputs.github_token }}") + arguments+=("--head-branch" "${{ steps.branch.outputs.head }}") + arguments+=("--target-branch" "${{ steps.branch.outputs.target }}") + arguments+=("--repository-name" "${{ inputs.repository_name }}") arguments+=("--working-directory" "${{ inputs.working_directory }}") arguments+=("--default-registry-domain" "${{ inputs.default_registry_domain }}") python -m "$module" "${arguments[@]}" - - name: Push changes - if: ${{ inputs.report_only == 'false' }} - working-directory: ${{ inputs.working_directory }} - shell: bash - run: | - git push diff --git a/infrapatch/action/__main__.py b/infrapatch/action/__main__.py index 9e70983..7658a1f 100644 --- a/infrapatch/action/__main__.py +++ b/infrapatch/action/__main__.py @@ -1,7 +1,10 @@ +import json +import subprocess from pathlib import Path import logging as log import click -import pygit2 +from github import Auth, Github +from github.PullRequest import PullRequest from infrapatch.core.composition import build_main_handler from infrapatch.core.log_helper import catch_exception, setup_logging @@ -14,10 +17,13 @@ @click.option("--default-registry-domain") @click.option("--registry-secrets-string", default=None) @click.option("--github-token") +@click.option("--target-branch") +@click.option("--head-branch") +@click.option("--repository-name") @click.option("--report-only", is_flag=True) @click.option("--working-directory") @catch_exception(handle=Exception) -def main(debug: bool, default_registry_domain: str, registry_secrets_string: str, github_token: str, report_only: bool, +def main(debug: bool, default_registry_domain: str, registry_secrets_string: str, github_token: str, target_branch: str, head_branch: str, repository_name: str, report_only: bool, working_directory: str): setup_logging(debug) log.debug(f"Running infrapatch with the following parameters: " @@ -45,7 +51,35 @@ def main(debug: bool, default_registry_domain: str, registry_secrets_string: str return main_handler.update_resources(upgradable_resources, True, working_directory, True) - main_handler.print_resource_table(upgradable_resources) + main_handler.dump_statistics(upgradable_resources, save_as_json_file=True) + + push_changes(target_branch, working_directory) + + create_pr(github_token, head_branch, repository_name, target_branch) + + +def push_changes(target_branch, working_directory): + command = ["git", "push", "-f", "-u", "origin", target_branch] + log.debug(f"Executing command: {' '.join(command)}") + try: + result = subprocess.run(command, capture_output=True, text=True, cwd=working_directory.absolute().as_posix()) + except Exception as e: + raise Exception(f"Error pushing to remote: {e}") + if result.returncode != 0: + log.error(f"Stdout: {result.stdout}") + raise Exception(f"Error pushing to remote: {result.stderr}") + + +def create_pr(github_token, head_branch, repository_name, target_branch) -> PullRequest: + token = Auth.Token(github_token) + github = Github(auth=token) + repo = github.get_repo(repository_name) + pull = repo.get_pulls(state='open', sort='created', base=head_branch, head=target_branch) + if pull.totalCount != 0: + log.info(f"Pull request found from '{target_branch}' to '{head_branch}'") + return pull[0] + log.info(f"No pull request found from '{target_branch}' to '{head_branch}'. Creating a new one.") + return repo.create_pull(title=f"InfraPatch Module and Provider Update", body=f"InfraPatch Module and Provider Update", base=head_branch, head=target_branch) def get_credentials_from_string(credentials_string: str) -> dict: diff --git a/infrapatch/cli/cli.py b/infrapatch/cli/cli.py index 4c283dc..ed56178 100644 --- a/infrapatch/cli/cli.py +++ b/infrapatch/cli/cli.py @@ -14,7 +14,7 @@ from infrapatch.core.utils.hcl_handler import HclHandler from infrapatch.core.utils.registry_handler import RegistryHandler -main_handler = None +main_handler: MainHandler = None @click.group(invoke_without_command=True) @@ -48,15 +48,14 @@ def report(project_root: str, only_upgradable: bool, dump_json_statistics: bool) @main.command() -@click.option("project_root", "--project-root", default=None, - help="Root directory of the project. If not specified, the current working directory is used. If commit-changes is set, this has to be the root of a git repository.") +@click.option("project_root", "--project-root", default=None, help="Root directory of the project. If not specified, the current working directory is used.") @click.option("--confirm", is_flag=True, help="Apply changes without confirmation.") @click.option("--dump-json-statistics", is_flag=True, help="Creates a json file containing statistics about the updated resources in the cwd.") -@click.option("--commit-changes", is_flag=True, help="Commits the changes to a git repository.") @catch_exception(handle=Exception) -def update(project_root: str, confirm: bool, dump_json_statistics: bool, commit_changes: bool): +def update(project_root: str, confirm: bool, dump_json_statistics: bool): + """Finds all modules and providers in the project_root and updates them to the newest version.""" if project_root is None: project_root = Path.cwd() global main_handler resources = main_handler.get_all_terraform_resources(Path(project_root)) - main_handler.update_resources(resources, confirm, Path(project_root), commit_changes) + main_handler.update_resources(resources, confirm, Path(project_root)) main_handler.dump_statistics(resources, dump_json_statistics) diff --git a/infrapatch/core/composition.py b/infrapatch/core/composition.py index 38b0078..f2bde81 100644 --- a/infrapatch/core/composition.py +++ b/infrapatch/core/composition.py @@ -3,6 +3,7 @@ from pathlib import Path import click +import rich from git import Repo from rich import progress from rich.console import Console @@ -23,13 +24,14 @@ def build_main_handler(default_registry_domain: str, credentials_file_path: str if credentials_dict is None: credentials_dict = get_registry_credentials(hcl_handler, credentials_file_path) registry_handler = RegistryHandler(default_registry_domain, credentials_dict) - return MainHandler(hcl_handler, registry_handler) + return MainHandler(hcl_handler, registry_handler, Console(width=cs.CLI_WIDTH)) class MainHandler: - def __init__(self, hcl_handler: HclHandler, registry_handler: RegistryHandler): + def __init__(self, hcl_handler: HclHandler, registry_handler: RegistryHandler, console: Console): self.hcl_handler = hcl_handler self.registry_handler = registry_handler + self._console = console def get_all_terraform_resources(self, project_root: Path) -> list[VersionedTerraformResource]: log.info(f"Searching for .tf files in {project_root.absolute().as_posix()} ...") @@ -87,8 +89,8 @@ def update_resources(self, resources: list[VersionedTerraformResource], confirm: if repo.bare: raise Exception("Working directory is not a git repository.") log.info(f"Committing changes to git branch '{repo.active_branch.name}'.") + self.print_resource_table(resources, True) if not confirm: - self.print_resource_table(resources, True) if not click.confirm("Do you want to apply the changes?"): print("Aborting...") return [] @@ -101,28 +103,30 @@ def update_resources(self, resources: list[VersionedTerraformResource], confirm: continue if commit_changes: repo.index.add([resource.source_file.absolute().as_posix()]) - repo.index.commit(f"Updated {resource.resource_name} '{resource.name}' from version '{resource.current_version}' to '{resource.newest_version}'.") + repo.index.commit(f"Bump {resource.resource_name} '{resource.name}' from version '{resource.current_version}' to '{resource.newest_version}'.") resource.set_patched() return upgradable_resources def _compose_resource_table(self, resources: list[VersionedTerraformResource], title: str): table = Table(show_header=True, title=title, - width=cs.CLI_WIDTH + expand=True ) - table.add_column("Name") - table.add_column("Current Version") - table.add_column("Newest Version") + table.add_column("Name", overflow="fold") + table.add_column("Source", overflow="fold") + table.add_column("Current") + table.add_column("Newest") table.add_column("Upgradeable") for resource in resources: + name = resource.identifier if isinstance(resource, TerraformProvider) else resource.name table.add_row( resource.name, + resource.source, resource.current_version, resource.newest_version, str(not resource.installed_version_equal_or_newer_than_new_version()) ) - console = Console() - console.print(table) + self._console.print(table) def dump_statistics(self, resources, save_as_json_file: bool = False): providers = [resource for resource in resources if isinstance(resource, TerraformProvider)] @@ -144,7 +148,7 @@ def dump_statistics(self, resources, save_as_json_file: bool = False): f.write(json.dumps(statistics)) table = Table(show_header=True, title="Statistics", - width=cs.CLI_WIDTH + expand=True ) table.add_column("Total Resources") table.add_column("Resources Pending Update") @@ -160,4 +164,4 @@ def dump_statistics(self, resources, save_as_json_file: bool = False): str(statistics["modules_count"]), str(statistics["providers_count"]) ) - Console().print(table) + self._console.print(table) diff --git a/infrapatch/core/constants.py b/infrapatch/core/constants.py index 92a5987..98620b1 100644 --- a/infrapatch/core/constants.py +++ b/infrapatch/core/constants.py @@ -1,5 +1,5 @@ # Width of the cli interface -CLI_WIDTH = 130 +CLI_WIDTH = 160 # Name of this App. Used all over the place in the cli APP_NAME = "InfraPatch" diff --git a/requirements.txt b/requirements.txt index 5f1de4d..d629124 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ pygohcl~=1.0.7 GitPython~=3.1.40 setuptools~=65.5.1 pygit2~=1.13.1 -semantic-version~=2.10.0 \ No newline at end of file +semantic-version~=2.10.0 +PyGithub~=2.1.1 \ No newline at end of file