diff --git a/README.md b/README.md index ab15bfd..e4852fe 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | field | required | default | description | |-------------------------------|----------|---------|-------------| -| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | +| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning, `repo:write`, and `workflow` privileges to create a pull request. | #### Other Configuration Options diff --git a/dependabot_file.py b/dependabot_file.py index 9edbc4d..92b0939 100644 --- a/dependabot_file.py +++ b/dependabot_file.py @@ -4,28 +4,29 @@ import yaml -def make_dependabot_config(ecosystem, group_dependencies) -> str: +def make_dependabot_config(ecosystem, group_dependencies, indent) -> str: """ Make the dependabot configuration for a specific package ecosystem Args: ecosystem: the package ecosystem to make the dependabot configuration for group_dependencies: whether to group dependencies in the dependabot.yml file + indent: the number of spaces to indent the dependabot configuration ex: " " Returns: str: the dependabot configuration for the package ecosystem """ - dependabot_config = f""" - package-ecosystem: '{ecosystem}' - directory: '/' - schedule: - interval: 'weekly' + dependabot_config = f"""{indent}- package-ecosystem: '{ecosystem}' +{indent}{indent}directory: '/' +{indent}{indent}schedule: +{indent}{indent}{indent}interval: 'weekly' """ if group_dependencies: - dependabot_config += """ groups: - production-dependencies: - dependency-type: 'production' - development-dependencies: - dependency-type: 'development' + dependabot_config += f"""{indent}{indent}groups: +{indent}{indent}{indent}production-dependencies: +{indent}{indent}{indent}{indent}dependency-type: 'production' +{indent}{indent}{indent}development-dependencies: +{indent}{indent}{indent}{indent}dependency-type: 'development' """ return dependabot_config @@ -58,10 +59,23 @@ def build_dependabot_file( "terraform": False, "github-actions": False, } + DEFAULT_INDENT = 2 # pylint: disable=invalid-name if existing_config: dependabot_file = existing_config.decoded.decode("utf-8") + ecosystem_line = next( + line + for line in dependabot_file.splitlines() + if "- package-ecosystem:" in line + ) + indent = " " * (len(ecosystem_line) - len(ecosystem_line.lstrip())) + if len(indent) < DEFAULT_INDENT: + print( + f"Invalid dependabot.yml file. No indentation found. Skipping {repo.full_name}" + ) + return None else: + indent = " " * DEFAULT_INDENT dependabot_file = """--- version: 2 updates: @@ -99,7 +113,7 @@ def build_dependabot_file( if repo.file_contents(file): package_managers_found[manager] = True dependabot_file += make_dependabot_config( - manager, group_dependencies + manager, group_dependencies, indent ) break except github3.exceptions.NotFoundError: @@ -112,7 +126,7 @@ def build_dependabot_file( if file[0].endswith(".tf"): package_managers_found["terraform"] = True dependabot_file += make_dependabot_config( - "terraform", group_dependencies + "terraform", group_dependencies, indent ) break except github3.exceptions.NotFoundError: @@ -123,7 +137,7 @@ def build_dependabot_file( if file[0].endswith(".yml") or file[0].endswith(".yaml"): package_managers_found["github-actions"] = True dependabot_file += make_dependabot_config( - "github-actions", group_dependencies + "github-actions", group_dependencies, indent ) break except github3.exceptions.NotFoundError: diff --git a/env.py b/env.py index 5aee695..e36709c 100644 --- a/env.py +++ b/env.py @@ -187,7 +187,7 @@ def get_env_vars(test: bool = False) -> tuple[ if len(commit_message) > 65536: raise ValueError("COMMIT_MESSAGE environment variable is too long") else: - commit_message = "Create dependabot.yaml" + commit_message = "Create/Update dependabot.yaml" created_after_date = os.getenv("CREATED_AFTER_DATE", "") is_match = re.match(r"\d{4}-\d{2}-\d{2}", created_after_date) diff --git a/evergreen.py b/evergreen.py index 12d6eda..83a6dfb 100644 --- a/evergreen.py +++ b/evergreen.py @@ -80,9 +80,11 @@ def main(): # pragma: no cover continue existing_config = None filename_list = [".github/dependabot.yml", ".github/dependabot.yaml"] + dependabot_filename_to_use = None for filename in filename_list: existing_config = check_existing_config(repo, filename, update_existing) if existing_config: + dependabot_filename_to_use = filename break if created_after_date and is_repo_created_date_before( @@ -130,7 +132,9 @@ def main(): # pragma: no cover body_issue = ( body + "\n\n```yaml\n" - + "# .github/dependabot.yml\n" + + "# " + + dependabot_filename_to_use + + "\n" + dependabot_file + "\n```" ) @@ -151,7 +155,13 @@ def main(): # pragma: no cover count_eligible += 1 try: pull = commit_changes( - title, body, repo, dependabot_file, commit_message + title, + body, + repo, + dependabot_file, + commit_message, + dependabot_filename_to_use, + existing_config, ) print("\tCreated pull request " + pull.html_url) if project_id: @@ -273,7 +283,15 @@ def check_pending_issues_for_duplicates(title, repo) -> bool: return skip -def commit_changes(title, body, repo, dependabot_file, message): +def commit_changes( + title, + body, + repo, + dependabot_file, + message, + dependabot_filename=".github/dependabot.yml", + existing_config=None, +): """Commit the changes to the repo and open a pull reques and return the pull request object""" default_branch = repo.default_branch # Get latest commit sha from default branch @@ -281,12 +299,19 @@ def commit_changes(title, body, repo, dependabot_file, message): front_matter = "refs/heads/" branch_name = "dependabot-" + str(uuid.uuid4()) repo.create_ref(front_matter + branch_name, default_branch_commit) - repo.create_file( - path=".github/dependabot.yaml", - message=message, - content=dependabot_file.encode(), # Convert to bytes object - branch=branch_name, - ) + if existing_config: + repo.file_contents(dependabot_filename).update( + message=message, + content=dependabot_file.encode(), # Convert to bytes object + branch=branch_name, + ) + else: + repo.create_file( + path=dependabot_filename, + message=message, + content=dependabot_file.encode(), # Convert to bytes object + branch=branch_name, + ) pull = repo.create_pull( title=title, body=body, head=branch_name, base=repo.default_branch diff --git a/test_dependabot_file.py b/test_dependabot_file.py index 42a8d80..a5ed65a 100644 --- a/test_dependabot_file.py +++ b/test_dependabot_file.py @@ -54,12 +54,15 @@ def test_build_dependabot_file_with_existing_config_bundler_no_update(self): result = build_dependabot_file(repo, False, [], existing_config) self.assertEqual(result, expected_result) - def test_build_dependabot_file_with_existing_config_bundler_with_update(self): + def test_build_dependabot_file_with_2_space_indent_existing_config_bundler_with_update( + self, + ): """Test that the dependabot.yml file is built correctly with bundler""" repo = MagicMock() repo.file_contents.side_effect = lambda f, filename="Gemfile": f == filename - # expected_result maintains existing ecosystem with custom configuration and adds new ecosystem + # expected_result maintains existing ecosystem with custom configuration + # and adds new ecosystem expected_result = """--- version: 2 updates: @@ -80,6 +83,21 @@ def test_build_dependabot_file_with_existing_config_bundler_with_update(self): result = build_dependabot_file(repo, False, [], existing_config) self.assertEqual(result, expected_result) + def test_build_dependabot_file_with_weird_space_indent_existing_config_bundler_with_update( + self, + ): + """Test that the dependabot.yml file is built correctly with bundler""" + repo = MagicMock() + repo.file_contents.side_effect = lambda f, filename="Gemfile": f == filename + + # expected_result maintains existing ecosystem with custom configuration + # and adds new ecosystem + existing_config = MagicMock() + existing_config.decoded = b'---\nversion: 2\nupdates:\n- package-ecosystem: "pip"\n directory: "/"\n\ + schedule:\n interval: "weekly"\n commit-message:\n prefix: "chore(deps)"\n' + result = build_dependabot_file(repo, False, [], existing_config) + self.assertEqual(result, None) + def test_build_dependabot_file_with_npm(self): """Test that the dependabot.yml file is built correctly with npm""" repo = MagicMock() diff --git a/test_env.py b/test_env.py index 26f1922..facaf1e 100644 --- a/test_env.py +++ b/test_env.py @@ -148,7 +148,7 @@ def test_get_env_vars_optional_values(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["internal", "private", "public"], @@ -187,7 +187,7 @@ def test_get_env_vars_with_update_existing(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["internal", "private", "public"], @@ -240,7 +240,7 @@ def test_get_env_vars_auth_with_github_app_installation(self): "secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["internal", "private", "public"], @@ -301,7 +301,7 @@ def test_get_env_vars_with_repos_no_dry_run(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["internal", "private", "public"], @@ -340,7 +340,7 @@ def test_get_env_vars_with_repos_disabled_security_updates(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["internal", "private", "public"], @@ -380,7 +380,7 @@ def test_get_env_vars_with_repos_filter_visibility_multiple_values(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["internal", "private"], @@ -420,7 +420,7 @@ def test_get_env_vars_with_repos_filter_visibility_single_value(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["public"], @@ -490,7 +490,7 @@ def test_get_env_vars_with_repos_filter_visibility_no_duplicates(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["private", "public"], @@ -531,7 +531,7 @@ def test_get_env_vars_with_repos_exempt_ecosystems(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["private", "public"], @@ -571,7 +571,7 @@ def test_get_env_vars_with_no_batch_size(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["private", "public"], @@ -612,7 +612,7 @@ def test_get_env_vars_with_batch_size(self): we can keep our dependencies up to date and secure.", "", False, - "Create dependabot.yaml", + "Create/Update dependabot.yaml", None, False, ["private", "public"], diff --git a/test_evergreen.py b/test_evergreen.py index b054954..1db2795 100644 --- a/test_evergreen.py +++ b/test_evergreen.py @@ -205,20 +205,28 @@ def test_commit_changes(self, mock_uuid): mock_repo.create_ref.return_value = True mock_repo.create_file.return_value = True mock_repo.create_pull.return_value = "MockPullRequest" + dependabot_file_name = ".github/dependabot.yml" title = "Test Title" body = "Test Body" dependabot_file = 'dependencies:\n - package_manager: "python"\n directory: "/"\n update_schedule: "live"' branch_name = "dependabot-12345678-1234-5678-1234-567812345678" - commit_message = "Create dependabot.yaml" - result = commit_changes(title, body, mock_repo, dependabot_file, commit_message) + commit_message = "Create " + dependabot_file_name + result = commit_changes( + title, + body, + mock_repo, + dependabot_file, + commit_message, + dependabot_file_name, + ) # Assert that the methods were called with the correct arguments mock_repo.create_ref.assert_called_once_with( f"refs/heads/{branch_name}", "abc123" ) mock_repo.create_file.assert_called_once_with( - path=".github/dependabot.yaml", + path=dependabot_file_name, message=commit_message, content=dependabot_file.encode(), branch=branch_name,