github-overlord 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.1
2
+ Name: github-overlord
3
+ Version: 0.2.0
4
+ Summary: Automatically merge Dependabot PRs in public repositories
5
+ Keywords: github,dependabot,automation
6
+ Author: Michael Bianco
7
+ Author-email: mike@mikebian.co
8
+ Requires-Python: >=3.12,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: click (>=8.1.7,<9.0.0)
12
+ Requires-Dist: funcy-pipe (>=0.11.0,<0.12.0)
13
+ Requires-Dist: pygithub (>=2.3.0,<3.0.0)
14
+ Requires-Dist: python-decouple-typed (>=3.11.0,<4.0.0)
15
+ Requires-Dist: structlog (>=24.2.0,<25.0.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/github-overlord)](https://github.com/iloveitaly/github-overlord/releases) [![Downloads](https://static.pepy.tech/badge/github-overlord/month)](https://pepy.tech/project/github-overlord) [![Python Versions](https://img.shields.io/pypi/pyversions/github-overlord)](https://pypi.org/project/github-overlord) ![GitHub CI Status](https://github.com/iloveitaly/github-overlord/actions/workflows/build_and_publish.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
19
+
20
+ # GitHub Overlord
21
+
22
+ GitHub Overlord is a Python script that automatically merges Dependabot PRs in public repositories that have passed CI checks.
23
+
24
+
25
+ ## Installation
26
+
27
+ ```shell
28
+ pip install github-overlord
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```shell
34
+ Usage: github-overlord [OPTIONS]
35
+
36
+ Automatically merge dependabot PRs in public repos that have passed CI
37
+ checks
38
+
39
+ Options:
40
+ --token TEXT GitHub token, can also be set via GITHUB_TOKEN
41
+ --dry-run Run script without merging PRs
42
+ --repo TEXT Only process a single repository
43
+ --help Show this message and exit.
44
+ ```
45
+
46
+ ### Docker Cron
@@ -0,0 +1,29 @@
1
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/github-overlord)](https://github.com/iloveitaly/github-overlord/releases) [![Downloads](https://static.pepy.tech/badge/github-overlord/month)](https://pepy.tech/project/github-overlord) [![Python Versions](https://img.shields.io/pypi/pyversions/github-overlord)](https://pypi.org/project/github-overlord) ![GitHub CI Status](https://github.com/iloveitaly/github-overlord/actions/workflows/build_and_publish.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ # GitHub Overlord
4
+
5
+ GitHub Overlord is a Python script that automatically merges Dependabot PRs in public repositories that have passed CI checks.
6
+
7
+
8
+ ## Installation
9
+
10
+ ```shell
11
+ pip install github-overlord
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```shell
17
+ Usage: github-overlord [OPTIONS]
18
+
19
+ Automatically merge dependabot PRs in public repos that have passed CI
20
+ checks
21
+
22
+ Options:
23
+ --token TEXT GitHub token, can also be set via GITHUB_TOKEN
24
+ --dry-run Run script without merging PRs
25
+ --repo TEXT Only process a single repository
26
+ --help Show this message and exit.
27
+ ```
28
+
29
+ ### Docker Cron
@@ -0,0 +1,145 @@
1
+ import os
2
+ import re
3
+ import time
4
+
5
+ import click
6
+ import funcy_pipe as fp
7
+ from github import Github
8
+ from github.GithubObject import NotSet
9
+ from github.PullRequest import PullRequest
10
+
11
+ from github_overlord.utils import log
12
+
13
+ AUTOMATIC_MERGE_MESSAGE = "Automatically merged with GitHub overlord"
14
+
15
+
16
+ def merge_pr(pr, dry_run):
17
+ if dry_run:
18
+ log.info("would merge PR", pr=pr.html_url)
19
+ return
20
+
21
+ pr.create_issue_comment(AUTOMATIC_MERGE_MESSAGE)
22
+ pr.merge(merge_method="squash")
23
+
24
+ log.info("merged PR", pr=pr.html_url)
25
+
26
+
27
+ def resolve_async_status(object, key):
28
+ """
29
+ https://github.com/PyGithub/PyGithub/issues/1979
30
+ """
31
+
32
+ count_limit = 10
33
+
34
+ while getattr(object, key) is None:
35
+ time.sleep(1)
36
+
37
+ setattr(object, f"_{key}", NotSet)
38
+ object._CompletableGithubObject__completed = False
39
+
40
+ count_limit -= 1
41
+ if count_limit == 0:
42
+ return
43
+
44
+
45
+ def is_eligible_for_merge(pr: PullRequest):
46
+ resolve_async_status(pr, "mergeable")
47
+
48
+ if not pr.mergeable:
49
+ return False
50
+
51
+ if pr.user.login != "dependabot[bot]":
52
+ return False
53
+
54
+ # pr_title = pr.title.lower()
55
+ # if "patch" not in pr_title and "github actions" not in pr_title:
56
+ # return False
57
+
58
+ last_commit = pr.get_commits().reversed[0]
59
+ combined_status = last_commit.get_combined_status()
60
+ status = combined_status.state
61
+
62
+ # status is different than CI runs!
63
+ if len(combined_status.statuses) > 0 and status != "success":
64
+ return False
65
+
66
+ all_checks_successful = (
67
+ last_commit.get_check_runs() | fp.pluck_attr("conclusion") | fp.all("success")
68
+ )
69
+
70
+ if not all_checks_successful:
71
+ return False
72
+
73
+ return True
74
+
75
+
76
+ def process_repo(repo, dry_run):
77
+ log.debug("checking repository", repo=repo.full_name)
78
+
79
+ if repo.fork:
80
+ log.debug("skipping forked repo")
81
+ return
82
+
83
+ pulls = repo.get_pulls(state="open")
84
+
85
+ if pulls.totalCount == 0:
86
+ log.debug("no open prs, skipping")
87
+ return
88
+
89
+ merged_pr_count = 0
90
+
91
+ for pr in pulls:
92
+ if is_eligible_for_merge(pr):
93
+ merge_pr(pr, dry_run)
94
+
95
+ merged_pr_count += 1
96
+ else:
97
+ log.debug("skipping PR", url=pr.html_url)
98
+
99
+ log.info("merged prs", count=merged_pr_count)
100
+
101
+
102
+ def main(token, dry_run, repo):
103
+ assert token, "GitHub token is required"
104
+
105
+ g = Github(token)
106
+ user = g.get_user()
107
+
108
+ if repo:
109
+ process_repo(g.get_repo(repo), dry_run)
110
+ return
111
+
112
+ # if not, process everything!
113
+ user.get_repos(type="public") | fp.filter(
114
+ lambda repo: repo.owner.login == user.login
115
+ ) | fp.map(fp.rpartial(process_repo, dry_run)) | fp.to_list()
116
+
117
+ log.info("dependabot pr check complete")
118
+
119
+
120
+ @click.command()
121
+ @click.option(
122
+ "--token",
123
+ help="GitHub token, can also be set via GITHUB_TOKEN",
124
+ default=os.getenv("GITHUB_TOKEN"),
125
+ )
126
+ @click.option("--dry-run", is_flag=True, help="Run script without merging PRs")
127
+ @click.option("--repo", help="Only process a single repository")
128
+ def cli(token, dry_run, repo):
129
+ """
130
+ Automatically merge dependabot PRs in public repos that have passed CI checks
131
+ """
132
+
133
+ if repo:
134
+ # convert 'https://github.com/iloveitaly/todoist-digest/pulls' to 'iloveitaly/todoist-digest'
135
+ if "github.com" in repo:
136
+ match = re.search(r"github\.com/([^/]+)/([^/]+)", repo)
137
+
138
+ if match:
139
+ repo = f"{match.group(1)}/{match.group(2)}"
140
+
141
+ main(token, dry_run, repo)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ cli()
@@ -0,0 +1,74 @@
1
+ """
2
+ This file should be imported first in any application entrypoint.
3
+ """
4
+
5
+ import logging
6
+ import typing as t
7
+ from pathlib import Path
8
+
9
+ import structlog
10
+ from decouple import config
11
+
12
+
13
+ def configure_logger():
14
+ logger_factory = structlog.PrintLoggerFactory()
15
+
16
+ # allow user to specify a log in case they want to do something meaningful with the stdout
17
+ if python_log_path := config("PYTHON_LOG_PATH", default=None):
18
+ python_log = open(
19
+ python_log_path, "a", encoding="utf-8"
20
+ ) # pylint: disable=consider-using-with
21
+ logger_factory = structlog.PrintLoggerFactory(file=python_log)
22
+
23
+ log_level = t.cast(str, config("LOG_LEVEL", default="INFO", cast=str))
24
+ level = getattr(logging, log_level.upper())
25
+
26
+ # TODO logging.root.manager.loggerDict
27
+ # we need this option to be set for other non-structlog loggers
28
+ logging.basicConfig(level=level)
29
+
30
+ # TODO look into further customized format
31
+ # https://cs.github.com/GeoscienceAustralia/digitalearthau/blob/4cf486eb2a93d7de23f86ce6de0c3af549fe42a9/digitalearthau/uiutil.py#L45
32
+
33
+ structlog.configure(
34
+ context_class=dict,
35
+ wrapper_class=structlog.make_filtering_bound_logger(level),
36
+ logger_factory=logger_factory,
37
+ cache_logger_on_first_use=True,
38
+ )
39
+
40
+
41
+ # must type manually, unfortunately :/
42
+ # https://www.structlog.org/en/21.3.0/types.html
43
+ log: structlog.stdlib.BoundLogger
44
+
45
+
46
+ def setup():
47
+ if hasattr(setup, "complete") and setup.complete:
48
+ return
49
+
50
+ global root, logger, log
51
+
52
+ root = Path(__file__).parent.parent
53
+
54
+ log = structlog.get_logger()
55
+
56
+ # context manager to auto-clear context
57
+ log.context = structlog.contextvars.bound_contextvars # type: ignore
58
+ # set thread-local context
59
+ log.local = structlog.contextvars.bind_contextvars # type: ignore
60
+ # clear thread-local context
61
+ log.clear = structlog.contextvars.clear_contextvars # type: ignore
62
+
63
+ logger = log
64
+
65
+ configure_logger()
66
+
67
+ log.info("application setup")
68
+
69
+ # local state in a method is strange, but it works :/
70
+ setup.complete = True
71
+
72
+
73
+ # side effects are bad, but it's fun to do bad things
74
+ setup()
@@ -0,0 +1,34 @@
1
+ [tool.poetry]
2
+ name = "github-overlord"
3
+ version = "0.2.0"
4
+ description = "Automatically merge Dependabot PRs in public repositories"
5
+ authors = ["Michael Bianco <mike@mikebian.co>"]
6
+ readme = "README.md"
7
+ keywords = ["github", "dependabot", "automation"]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.12"
11
+ pygithub = "^2.3.0"
12
+ click = "^8.1.7"
13
+ python-decouple-typed = "^3.11.0"
14
+ funcy-pipe = "^0.11.0"
15
+ structlog = "^24.2.0"
16
+
17
+ [tool.poetry.scripts]
18
+ github-overlord = "github_overlord:cli"
19
+
20
+
21
+ [tool.isort]
22
+ profile = "black"
23
+ # helpful when copy/pasting GPT code
24
+ float_to_top = true
25
+
26
+ [tool.poetry.group.dev.dependencies]
27
+ isort = "^5.13.2"
28
+ black = "^24.4.2"
29
+ pyright = "^1.1.366"
30
+ pylint = "^3.2.3"
31
+
32
+ [build-system]
33
+ requires = ["poetry-core"]
34
+ build-backend = "poetry.core.masonry.api"