github-rest-api 0.45.0__py3-none-any.whl → 0.47.0__py3-none-any.whl

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.
@@ -83,9 +83,15 @@ def deterministic_title(compare: dict[str, Any]) -> str:
83
83
  """Derive a Conventional-Commits title from a comparison result.
84
84
 
85
85
  The type is the most significant type present across the commits
86
- (``feat`` > ``fix`` > the most frequent parsed type > ``chore``); the scope
87
- is the common top-level directory of the changed files; ``!`` is appended
88
- for breaking changes.
86
+ (``feat`` > ``fix`` > the most frequent parsed type). When no commit follows
87
+ the Conventional-Commits grammar, the type is ``ci`` for changes confined to
88
+ ``.github`` and ``chore`` otherwise. The scope is the common top-level
89
+ directory of the changed files; ``!`` is appended for breaking changes.
90
+
91
+ The description reuses a matching commit's description when available and
92
+ otherwise falls back to the first commit's subject line, so a genuinely
93
+ descriptive (if non-conventional) subject is preserved rather than replaced
94
+ by a generic placeholder.
89
95
 
90
96
  :param compare: The comparison result from `Repository.compare`.
91
97
  """
@@ -94,17 +100,22 @@ def deterministic_title(compare: dict[str, Any]) -> str:
94
100
  parsed = [parse_conventional(subject) for subject in subjects]
95
101
  types = [item[0] for item in parsed if item]
96
102
  scope = _common_scope(compare)
97
- prefix_scope = f"({scope})" if scope else ""
98
- if not subjects:
99
- return f"chore{prefix_scope}: update"
100
103
  if "feat" in types:
101
104
  type_ = "feat"
102
105
  elif "fix" in types:
103
106
  type_ = "fix"
104
107
  elif types:
105
108
  type_ = Counter(types).most_common(1)[0][0]
109
+ elif scope == ".github":
110
+ # Changes confined to `.github` (e.g. workflow files) are conventionally
111
+ # `ci`; the type already conveys the location, so drop the (redundant)
112
+ # `.github` scope.
113
+ type_, scope = "ci", None
106
114
  else:
107
115
  type_ = "chore"
116
+ prefix_scope = f"({scope})" if scope else ""
117
+ if not subjects:
118
+ return f"{type_}{prefix_scope}: update"
108
119
  breaking = any(item[2] for item in parsed if item) or any(
109
120
  _BREAKING_CHANGE_PATTERN.search(message) for message in messages
110
121
  )
@@ -113,7 +124,7 @@ def deterministic_title(compare: dict[str, Any]) -> str:
113
124
  else:
114
125
  description = next(
115
126
  (item[3] for item in parsed if item and item[0] == type_),
116
- f"update {len(subjects)} commits",
127
+ subjects[0],
117
128
  )
118
129
  return f"{type_}{prefix_scope}{'!' if breaking else ''}: {description}"
119
130
 
@@ -0,0 +1,109 @@
1
+ """Auto-merge eligible open pull requests in the repository.
2
+
3
+ Every open PR is evaluated by ``Repository.auto_merge_pull_requests`` and merged
4
+ only when it passes the eligibility gate (author allowlist, title type, head
5
+ commit age, mergeable state and an approving review or marker comment).
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import sys
11
+ from argparse import ArgumentParser, Namespace
12
+
13
+ from github_rest_api import MergeMethod, Repository
14
+ from github_rest_api.github import (
15
+ DEFAULT_AUTO_MERGE_MARKER,
16
+ DEFAULT_AUTO_MERGE_TYPES,
17
+ DEFAULT_MIN_AGE_MINUTES,
18
+ )
19
+
20
+
21
+ def parse_args(args=None, namespace=None) -> Namespace:
22
+ """Parse command-line arguments.
23
+ :param args: The arguments to parse.
24
+ If None, the arguments from command-line are parsed.
25
+ :param namespace: An inital Namespace object.
26
+ :return: A namespace object containing parsed options.
27
+ """
28
+ parser = ArgumentParser(description="Auto-merge eligible open pull requests.")
29
+ parser.add_argument(
30
+ "--token",
31
+ dest="token",
32
+ required=True,
33
+ help="The personal access token for authentication.",
34
+ )
35
+ parser.add_argument(
36
+ "--authors",
37
+ dest="authors",
38
+ nargs="*",
39
+ default=[],
40
+ help="Logins whose PRs are eligible for auto-merge. An empty allowlist "
41
+ "makes nothing eligible (fail-safe).",
42
+ )
43
+ parser.add_argument(
44
+ "--approvers",
45
+ dest="approvers",
46
+ nargs="*",
47
+ default=[],
48
+ help="Logins whose reviews or marker comments grant approval.",
49
+ )
50
+ parser.add_argument(
51
+ "--allowed-types",
52
+ dest="allowed_types",
53
+ nargs="*",
54
+ default=list(DEFAULT_AUTO_MERGE_TYPES),
55
+ help="Conventional-Commits title types eligible for auto-merge.",
56
+ )
57
+ parser.add_argument(
58
+ "--min-age-minutes",
59
+ dest="min_age_minutes",
60
+ type=int,
61
+ default=DEFAULT_MIN_AGE_MINUTES,
62
+ help="The minimum head-commit age in minutes; set to 0 to disable the guard.",
63
+ )
64
+ parser.add_argument(
65
+ "--marker",
66
+ dest="marker",
67
+ default=DEFAULT_AUTO_MERGE_MARKER,
68
+ help="The marker substring an approver may comment to approve.",
69
+ )
70
+ parser.add_argument(
71
+ "--merge-method",
72
+ dest="merge_method",
73
+ choices=[m.value for m in MergeMethod],
74
+ default=MergeMethod.MERGE.value,
75
+ help="The merge method to use.",
76
+ )
77
+ parser.add_argument(
78
+ "--dry-run",
79
+ dest="dry_run",
80
+ action="store_true",
81
+ help="Log the PRs that would be merged without merging them.",
82
+ )
83
+ return parser.parse_args(args=args, namespace=namespace)
84
+
85
+
86
+ def main() -> int:
87
+ """Main entrance of the script,
88
+ which auto-merges every eligible open pull request in the repository.
89
+ """
90
+ args = parse_args()
91
+ # auto_merge_pull_requests reports its outcomes (including the dry-run
92
+ # listing) via logging.info, which the root logger suppresses by default;
93
+ # configure logging so those messages are actually emitted.
94
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
95
+ repo = Repository(args.token, os.environ["GITHUB_REPOSITORY"])
96
+ repo.auto_merge_pull_requests(
97
+ authors=args.authors,
98
+ approvers=args.approvers,
99
+ allowed_types=args.allowed_types,
100
+ min_age_minutes=args.min_age_minutes,
101
+ marker=args.marker,
102
+ merge_method=args.merge_method,
103
+ dry_run=args.dry_run,
104
+ )
105
+ return 0
106
+
107
+
108
+ if __name__ == "__main__":
109
+ sys.exit(main())
@@ -38,6 +38,13 @@ def _remote_url(repo: str, protocol: str) -> str:
38
38
  return f"https://github.com/{repo}.git"
39
39
 
40
40
 
41
+ def _ensure_remote(path: Path, repo: str, protocol: str) -> None:
42
+ try:
43
+ porcelain.remote_add(path, "origin", _remote_url(repo, protocol))
44
+ except porcelain.RemoteExists:
45
+ pass
46
+
47
+
41
48
  def _init_local_repo(
42
49
  repo: str,
43
50
  language: str,
@@ -54,33 +61,34 @@ def _init_local_repo(
54
61
  readme.write_text(f"# {repo_name}\n")
55
62
  if not (path / ".git").exists():
56
63
  porcelain.init(path=path)
57
- porcelain.remote_add(path, "origin", _remote_url(repo, protocol))
58
64
  initial_branch = (
59
65
  (path / ".git" / "HEAD").read_text().strip().partition("refs/heads/")[-1]
60
66
  )
61
67
  porcelain.add(repo=path)
62
68
  porcelain.commit(repo=path, message="first commit")
63
-
64
- def _create_push_branch(branch: str):
69
+ for branch in branches:
65
70
  if branch != initial_branch:
66
71
  porcelain.branch_create(repo=path, name=branch)
67
- porcelain.checkout(repo=path, target=branch)
68
- porcelain.push(
69
- repo=path,
70
- remote_location=_remote_url(repo, "https"),
71
- username="x-access-token",
72
- password=token,
73
- )
74
-
75
- for branch in branches:
76
- _create_push_branch(branch)
77
72
  porcelain.checkout(repo=path, target=branches[0])
78
73
  if initial_branch not in branches:
79
74
  porcelain.branch_delete(repo=path, name=initial_branch)
75
+ _ensure_remote(path, repo, protocol)
76
+ local_branches = {b.decode() for b in porcelain.branch_list(path)}
77
+ branches_to_push = [branch for branch in branches if branch in local_branches]
78
+ if not branches_to_push:
79
+ branches_to_push = [porcelain.active_branch(path).decode()]
80
+ for branch in branches_to_push:
81
+ porcelain.push(
82
+ repo=path,
83
+ remote_location=_remote_url(repo, "https"),
84
+ refspecs=[branch.encode()],
85
+ username="x-access-token",
86
+ password=token,
87
+ )
80
88
  _add_workflow(path, language)
81
89
 
82
90
 
83
- def add_github_repo(
91
+ def create_github_repo(
84
92
  repo: str,
85
93
  private: bool,
86
94
  language: str,
@@ -117,7 +125,7 @@ def _add_workflow(path: Path, language: str, workflow_dir: Path | None = None) -
117
125
  workflow_dir = Path(__file__).parent / "workflows"
118
126
  dir_dest = path / ".github" / "workflows"
119
127
  dir_dest.mkdir(parents=True, exist_ok=True)
120
- for yaml in workflow_dir.glob("*.yml"):
128
+ for yaml in workflow_dir.glob("*.yaml"):
121
129
  if not (dir_dest / yaml.name).exists():
122
130
  shutil.copy2(yaml, dir_dest)
123
131
  if not language:
@@ -125,13 +133,13 @@ def _add_workflow(path: Path, language: str, workflow_dir: Path | None = None) -
125
133
  lang_dir = workflow_dir / language
126
134
  if not lang_dir.exists():
127
135
  return
128
- for yaml in lang_dir.glob("*.yml"):
136
+ for yaml in lang_dir.glob("*.yaml"):
129
137
  if not (dir_dest / yaml.name).exists():
130
138
  shutil.copy2(yaml, dir_dest)
131
139
 
132
140
 
133
141
  def parse_args(args=None, namespace=None):
134
- parser = argparse.ArgumentParser(description="Add a GitHub repository.")
142
+ parser = argparse.ArgumentParser(description="Create a GitHub repository.")
135
143
  parser.add_argument(
136
144
  "repo",
137
145
  help="The GitHub repo (in the format of owner/repo) to be created.",
@@ -203,7 +211,7 @@ def parse_args(args=None, namespace=None):
203
211
  def main() -> int:
204
212
  args = parse_args()
205
213
  try:
206
- add_github_repo(
214
+ create_github_repo(
207
215
  repo=args.repo,
208
216
  private=args.private,
209
217
  language=args.language,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-rest-api
3
- Version: 0.45.0
3
+ Version: 0.47.0
4
4
  Summary: Simple wrapper of GitHub REST APIs.
5
5
  Author-email: Ben Du <longendu@yahoo.com>
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -1,6 +1,6 @@
1
1
  github_rest_api/__init__.py,sha256=VVpiUHVLbjjJGNN88OmEvCwq4xmBnXv68T8Apk_lwVc,282
2
2
  github_rest_api/github.py,sha256=iAfhPLH9UGPtScZfcV1TCeY5XfUzAMqenf5FrSNplQc,45146
3
- github_rest_api/pr_content.py,sha256=8SJKdTOCTt1pZasmh3MEQ2sRXlVX2dYGpYHZP9H5K3g,9415
3
+ github_rest_api/pr_content.py,sha256=og1VwsMtwqwl-HUJcJrkwjUjFPw9E1k5xDtJVMAXBRo,10039
4
4
  github_rest_api/utils.py,sha256=RspoIZik0g105KpyFXCf4epAtgu1dUsxwlwUSSGhXPI,2697
5
5
  github_rest_api/scripts/__init__.py,sha256=NkHTngnnWXPc0JmO5mqVKjDf4JPofuwWjDW1SU3aE7Y,36
6
6
  github_rest_api/scripts/utils.py,sha256=o5GzgSNyGHUIHhaaG5QUfXvv6fsTQPCHD9clGRNMggU,4588
@@ -13,7 +13,8 @@ github_rest_api/scripts/container/build_container_images.py,sha256=-ZPPKVUpc0v-_
13
13
  github_rest_api/scripts/container/config_container.py,sha256=BtOqgZlQFM9cWZMoHKvOzNsyXxCl_DLqYWbMqu1YOWM,2835
14
14
  github_rest_api/scripts/container/update_version_containerfile.py,sha256=1PkuJkAsJBCEMdacMSndbuZKcmgUyXy6nBArnxKoPZk,4535
15
15
  github_rest_api/scripts/github/__init__.py,sha256=tEU1SdCS51M4M13WoqXZt3rjGEAI_QgNljt5XAyQJ_w,38
16
- github_rest_api/scripts/github/add_github_repo.py,sha256=iFuDGXYz_bCXo9_1Y4TMleSZ4HFHriNOT9oj4-XAnno,6351
16
+ github_rest_api/scripts/github/auto_merge_pull_request.py,sha256=_5191Xt_b8DtxjIqHrdY-gKLxvkzWd8oWoSbF2uauc8,3527
17
+ github_rest_api/scripts/github/create_github_repo.py,sha256=7t2dgDcLrLPpBY5CpTtkkIe8Z_-2FGgoRo_--HXwcuw,6686
17
18
  github_rest_api/scripts/github/create_pull_request.py,sha256=gXtRRsxzegGxagHb6k3kxtRd3mt4QVwSPZQpsFnzz1Y,3968
18
19
  github_rest_api/scripts/github/release_on_github.py,sha256=9psaIv-GtrHdvE-tkw0yxCMDmW0bPts-db2vUxhoKXo,4368
19
20
  github_rest_api/scripts/github/remove_branch.py,sha256=iKZ4Y_lQlSOIfifC4plqP-B6pEMnFrknGMvV1oLnuLc,3538
@@ -23,7 +24,7 @@ github_rest_api/scripts/github/workflows/create_pr_to_main.yaml,sha256=mgHPlewkC
23
24
  github_rest_api/scripts/github/workflows/remove_branch.yaml,sha256=KDpB76ovrhuRdP0HN5j6Th9gjIQWSDdh8b4b4yAgtoI,544
24
25
  github_rest_api/scripts/github/workflows/python/lint.yaml,sha256=toP2oUdVIKyu_n20Dr14k5cJYxN8rPksfbX4sfflXxc,699
25
26
  github_rest_api/scripts/github/workflows/python/test.yaml,sha256=86MljYEWX1E9kJYmhMCLwu46WNEWvoyO9PwqeRew540,887
26
- github_rest_api-0.45.0.dist-info/METADATA,sha256=wvu0t0cqklG5gPmrq_0QmbyOR5frDuhAjyMbAmf8SVI,915
27
- github_rest_api-0.45.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
28
- github_rest_api-0.45.0.dist-info/entry_points.txt,sha256=eXMEdbMEpuE1nsTfUiKSILgS4awe09NwKn0N2_fZ9H0,567
29
- github_rest_api-0.45.0.dist-info/RECORD,,
27
+ github_rest_api-0.47.0.dist-info/METADATA,sha256=SjlTYqUbYuVm5n3gvDE7NE1PutETNTXXoV609PLMOsc,915
28
+ github_rest_api-0.47.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
29
+ github_rest_api-0.47.0.dist-info/entry_points.txt,sha256=OfbP9GuQC7SyGbymfCYrXEG-TCcKuu31v0EPNvHc1Q4,659
30
+ github_rest_api-0.47.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,7 +1,8 @@
1
1
  [console_scripts]
2
- add_github_repo = github_rest_api.scripts.github.add_github_repo:main
2
+ auto_merge_pull_request = github_rest_api.scripts.github.auto_merge_pull_request:main
3
3
  build_container_images = github_rest_api.scripts.container.build_container_images:main
4
4
  config_container = github_rest_api.scripts.container.config_container:main
5
+ create_github_repo = github_rest_api.scripts.github.create_github_repo:main
5
6
  create_pull_request = github_rest_api.scripts.github.create_pull_request:main
6
7
  release_on_github = github_rest_api.scripts.github.release_on_github:main
7
8
  remove_branch = github_rest_api.scripts.github.remove_branch:main