Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: maintain dependabot config filename for existing configs #150

Merged
merged 8 commits into from
May 30, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 27 additions & 13 deletions dependabot_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 34 additions & 9 deletions evergreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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```"
)
Expand All @@ -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:
Expand Down Expand Up @@ -273,20 +283,35 @@ 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
default_branch_commit = repo.ref("heads/" + default_branch).object.sha
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
Expand Down
22 changes: 20 additions & 2 deletions test_dependabot_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Expand Down
22 changes: 11 additions & 11 deletions test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
14 changes: 11 additions & 3 deletions test_evergreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down