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

feat(dependabot): Automate dependabot reviews #33067

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/add_dependabot_pr_to_mq.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: Add dependabot PR to the Merge Queue
on:
pull_request_review:
types:
- submitted
- edited

jobs:
add_to_merge_queue:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write

steps:
- uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ vars.DD_GITHUB_TOKEN_GENERATOR_APP_ID }}
private-key: ${{ secrets.DD_GITHUB_TOKEN_GENERATOR_PRIVATE_KEY }}
- name: Check if the PR is mergeable
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
id: check-mergeable
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const pullRequestNumber = context.payload.pull_request.number;
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequestNumber
});

return `${pullRequest.mergeable && pullRequest.mergeable_state === 'clean'}`;
result-encoding: string
- name: Contact agent-devx
if: ${{ steps.check-mergeable.outputs.result != 'true' }}
run: |
message="Hi!\nThis dependabot PR ${{github.event.pull_request.html_url}} is not mergeable.\nPlease check it out."
curl -X POST -H "Content-Type: application/json" --data '{"message": "$message"}' ${{ secrets.SLACK_AGENT_DEVX_WEBHOOK }}
- name: Add Merge Comment to Pull Request
if: ${{ steps.check-mergeable.outputs.result == 'true' }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const pullRequestNumber = context.payload.pull_request.number;
const commentBody = "/merge";

// Add a comment to the pull request
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequestNumber,
body: commentBody
});
42 changes: 42 additions & 0 deletions .github/workflows/ask_dependabot_pr_review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
name: "Add reviewers and ask review for dependabot PR"

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- "[0-9]+.[0-9]+.x"
pull_request_review_comment:
types: [created, deleted]

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

permissions: {}
jobs:
add_reviewers:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Setup python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version-file: .python-version
cache: 'pip'
cache-dependency-path: '**/requirements*.txt'

- name: Install dependencies
run: pip install -r requirements.txt -r tasks/requirements.txt

- name: Add reviewers and ask review
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: inv -e issue.add-reviewers -p $PR_NUMBER
50 changes: 50 additions & 0 deletions tasks/issue.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import os
import random
import re

from invoke import task

from tasks.libs.ciproviders.github_api import GithubAPI, ask_review_actor
from tasks.libs.issue.assign import assign_with_model, assign_with_rules
from tasks.libs.issue.model.actions import fetch_data_and_train_model
from tasks.libs.owners.parsing import search_owners
from tasks.libs.pipeline.notifications import GITHUB_SLACK_MAP, GITHUB_SLACK_REVIEW_MAP, HELP_SLACK_CHANNEL


Expand Down Expand Up @@ -101,3 +103,51 @@ def ask_reviews(_, pr_id):
except Exception as e:
message = f"An error occurred while sending a review message from {actor} for PR <{pr.html_url}/s|{pr.title}> to channel {channel}. Error: {e}"
client.chat_postMessage(channel='#agent-devx-ops', text=message)


@task
def add_reviewers(ctx, pr_id):
"""
Add team labels and reviewers to a dependabot bump PR based on the changed dependencies
"""

gh = GithubAPI()
pr = gh.repo.get_pull(int(pr_id))

if pr.user.login != "dependabot[bot]":
print("This is not a (dependabot) bump PR, this action should not be run on it.")
return

folder = ""
if pr.title.startswith("Bump the "):
match = re.match(r"^Bump the (\S+) group (.*$)", pr.title)
if match.group(2).startswith("in"):
match_folder = re.match(r"^in (\S+).*$", match.group(2))
folder = match_folder.group(1).removeprefix("/")
else:
match = re.match(r"^Bump (\S+) from (\S+) to (\S+)$", pr.title)
dependency = match.group(1)

# Find the responsible person for each file
owners = set()
git_files = ctx.run("git ls-files | grep -e \"^.*.go$\"", hide=True).stdout
for file in git_files.splitlines():
if not file.startswith(folder):
continue
in_import = False
with open(file) as f:
for line in f:
# Look for the import block
if "import (" in line:
in_import = True
if in_import:
# Early exit at the end of the import block
if ")" in line:
break
else:
if re.search(dependency, line):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be something like this right?

Suggested change
if re.search(dependency, line):
if dependency in line:

owners.update(set(search_owners(file, ".github/CODEOWNERS")))
break
# Teams are added by slug, so we need to remove the @DataDog/ prefix
pr.create_review_request(team_reviewers=[owner.casefold().removeprefix("@datadog/") for owner in owners])
pr.add_to_labels("ask-review")
2 changes: 2 additions & 0 deletions tasks/libs/owners/parsing.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

from collections import Counter
from functools import lru_cache
from typing import Any


@lru_cache
def read_owners(owners_file: str, remove_default_pattern=False) -> Any:
"""
- remove_default_pattern: If True, will remove the '*' entry
Expand Down
173 changes: 172 additions & 1 deletion tasks/unit_tests/issue_tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

from invoke.context import MockContext, Result

from tasks.issue import add_reviewers
from tasks.libs.issue.assign import guess_from_keywords, guess_from_labels


Expand Down Expand Up @@ -34,3 +37,171 @@ def test_with_a_file(self):
def test_no_match(self):
issue = MagicMock(title="fix bug", body="It comes from the file... hm I don't know.")
self.assertEqual(guess_from_keywords(issue), "triage")


class TestAddReviewers(unittest.TestCase):
@patch('builtins.print')
@patch('tasks.issue.GithubAPI')
def test_dependabot_only(self, gh_mock, print_mock):
pr_mock = MagicMock()
pr_mock.user.login = "InvisibleMan"
gh_mock.repo.get_pull.return_value = pr_mock
c = MockContext()
add_reviewers(c, 1234)
print_mock.assert_called_once_with("This is not a (dependabot) bump PR, this action should not be run on it.")

@patch('builtins.print')
@patch('tasks.issue.GithubAPI')
def test_single_dependency_one_reviewer(self, gh_mock, print_mock):
pr_mock = MagicMock()
pr_mock.user.login = "dependabot[bot]"
pr_mock.title = "Bump modernc.org/mathutil from 1.6.0 to 1.7.0"
gh_instance = MagicMock()
gh_instance.repo.get_pull.return_value = pr_mock
gh_mock.return_value = gh_instance
c = MockContext(
run={
"git ls-files | grep -e \"^.*.go$\"": Result("""pkg/security/proto/ebpfless/msg.go
pkg/security/ptracer/span.go
pkg/security/secl/go.mod
pkg/security/secl/model/model.go
pkg/security/secl/model/model_helpers_unix.go
pkg/security/secl/model/model_unix.go
""")
}
)
add_reviewers(c, 1234)
print_mock.assert_not_called()
pr_mock.create_review_request.assert_called_once_with(team_reviewers=['agent-security'])

@patch('builtins.print')
@patch('tasks.issue.GithubAPI')
def test_single_dependency_several_reviewers(self, gh_mock, print_mock):
pr_mock = MagicMock()
pr_mock.user.login = "dependabot[bot]"
pr_mock.title = "Bump github.com/go-delve/delve from 1.6.0 to 1.7.0"
gh_instance = MagicMock()
gh_instance.repo.get_pull.return_value = pr_mock
gh_mock.return_value = gh_instance
c = MockContext(
run={
"git ls-files | grep -e \"^.*.go$\"": Result("""generate_tools.go
pkg/dynamicinstrumentation/diconfig/dwarf.go
pkg/network/go/asmscan/scan.go
pkg/network/go/bininspect/dwarf.go
pkg/network/go/bininspect/newproc.go
pkg/network/go/bininspect/types.go
pkg/network/go/bininspect/utils.go
pkg/network/go/dwarfutils/compile_unit.go
pkg/network/go/dwarfutils/locexpr/exec.go
pkg/network/go/dwarfutils/type_finder.go
pkg/network/go/goid/goid_offset.go
pkg/network/go/goversion/version.go
pkg/network/go/lutgen/run.go
pkg/network/protocols/http/gotls/lookup/luts.go""")
}
)
add_reviewers(c, 1234)
print_mock.assert_not_called()
self.assertCountEqual(
pr_mock.create_review_request.call_args[1]['team_reviewers'],
['universal-service-monitoring', 'debugger', 'agent-devx-infra'],
)

@patch('builtins.print')
@patch('tasks.issue.GithubAPI')
def test_group_dependency(self, gh_mock, print_mock):
pr_mock = MagicMock()
pr_mock.user.login = "dependabot[bot]"
pr_mock.title = "Bump the aws-sdk-go-v2 group with 5 updates"
gh_instance = MagicMock()
gh_instance.repo.get_pull.return_value = pr_mock
gh_mock.return_value = gh_instance
c = MockContext(
run={
"git ls-files | grep -e \"^.*.go$\"": Result("""pkg/databasemonitoring/aws/aurora.go
pkg/databasemonitoring/aws/aurora_test.go
pkg/databasemonitoring/aws/client.go
pkg/databasemonitoring/aws/rdsclient_mockgen.go
pkg/serverless/apikey/api_key.go
pkg/serverless/apikey/api_key_test.go
pkg/serverless/trace/inferredspan/propagation_test.go
pkg/serverless/trace/propagation/carriers_test.go
pkg/serverless/trace/propagation/extractor_test.go
pkg/serverless/trigger/extractor.go
pkg/util/ec2/ec2_tags.go
test/new-e2e/examples/ecs_test.go
test/new-e2e/go.mod
test/new-e2e/pkg/provisioners/aws/kubernetes/kubernetes_dump.go
test/new-e2e/pkg/runner/parameters/store_aws.go
test/new-e2e/pkg/utils/clients/aws.go
test/new-e2e/pkg/utils/e2e/client/ecs/ecs.go
test/new-e2e/pkg/utils/e2e/client/ecs/session-manager-plugin.go
test/new-e2e/tests/containers/ecs_test.go
test/new-e2e/tests/windows/common/pipeline/pipeline.go""")
}
)
add_reviewers(c, 1234)
print_mock.assert_not_called()
self.assertCountEqual(
pr_mock.create_review_request.call_args[1]['team_reviewers'],
[
'windows-agent',
'database-monitoring',
'container-integrations',
'agent-devx-loops',
'serverless',
'container-platform',
'windows-kernel-integrations',
'agent-shared-components',
'agent-e2e-testing',
'serverless-aws',
],
)

@patch('builtins.print')
@patch('tasks.issue.GithubAPI')
def test_group_dependency_scoped(self, gh_mock, print_mock):
pr_mock = MagicMock()
pr_mock.user.login = "dependabot[bot]"
pr_mock.title = "Bump the aws-sdk-go-v2 group in /test/new-e2e with 5 updates"
gh_instance = MagicMock()
gh_instance.repo.get_pull.return_value = pr_mock
gh_mock.return_value = gh_instance
c = MockContext(
run={
"git ls-files | grep -e \"^.*.go$\"": Result("""pkg/databasemonitoring/aws/aurora.go
pkg/databasemonitoring/aws/aurora_test.go
pkg/databasemonitoring/aws/client.go
pkg/databasemonitoring/aws/rdsclient_mockgen.go
pkg/serverless/apikey/api_key.go
pkg/serverless/apikey/api_key_test.go
pkg/serverless/trace/inferredspan/propagation_test.go
pkg/serverless/trace/propagation/carriers_test.go
pkg/serverless/trace/propagation/extractor_test.go
pkg/serverless/trigger/extractor.go
pkg/util/ec2/ec2_tags.go
test/new-e2e/examples/ecs_test.go
test/new-e2e/go.mod
test/new-e2e/pkg/provisioners/aws/kubernetes/kubernetes_dump.go
test/new-e2e/pkg/runner/parameters/store_aws.go
test/new-e2e/pkg/utils/clients/aws.go
test/new-e2e/pkg/utils/e2e/client/ecs/ecs.go
test/new-e2e/pkg/utils/e2e/client/ecs/session-manager-plugin.go
test/new-e2e/tests/containers/ecs_test.go
test/new-e2e/tests/windows/common/pipeline/pipeline.go""")
}
)
add_reviewers(c, 1234)
print_mock.assert_not_called()
self.assertCountEqual(
pr_mock.create_review_request.call_args[1]['team_reviewers'],
[
'windows-agent',
'container-integrations',
'agent-devx-loops',
'container-platform',
'windows-kernel-integrations',
'agent-e2e-testing',
],
)
Loading