github-rest-api 0.47.1__py3-none-any.whl → 0.48.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.
github_rest_api/github.py CHANGED
@@ -31,9 +31,9 @@ DEFAULT_PR_MODEL = "anthropic/claude-haiku-4-5-20251001"
31
31
  # Defaults for automatic pull request merging (see Repository.auto_merge_pull_requests).
32
32
  # A marker comment from an allowlisted approver is accepted as an approval signal
33
33
  # for AI reviewers that only comment instead of submitting a formal review.
34
- DEFAULT_AUTO_MERGE_MARKER = "<!-- auto-merge: approved -->"
34
+ DEFAULT_AUTO_MERGE_MARKER = "AUTO_MERGE_APPROVED"
35
35
  # Conventional-commit title types eligible for auto-merge.
36
- DEFAULT_AUTO_MERGE_TYPES = ("chore", "docs", "deps")
36
+ DEFAULT_AUTO_MERGE_TYPES = ("build", "chore", "ci", "docs", "deps")
37
37
  # A PR is only auto-merged once its head commit is at least this old, an anti-race
38
38
  # guard so a PR that momentarily reads `clean` before CI registers is not merged.
39
39
  # A just-created PR is simply deferred to the next run, never dropped, so a small
@@ -177,8 +177,15 @@ def _field_gate_failure(
177
177
  return "it is a draft"
178
178
  if not _in_allowlist(_login(pr), _normalized_logins(authors)):
179
179
  return "its author is not in the allowlist"
180
- if not _title_type_allowed(pr.get("title") or "", allowed_types):
181
- return "its title type is not auto-merge eligible"
180
+ title = pr.get("title") or ""
181
+ if not _title_type_allowed(title, allowed_types):
182
+ type_ = _conventional_type(title)
183
+ current = f"'{type_}'" if type_ is not None else "missing/unrecognized"
184
+ eligible = ", ".join(allowed_types) or "none"
185
+ return (
186
+ f"its title type ({current}) is not auto-merge eligible "
187
+ f"(eligible types: {eligible})"
188
+ )
182
189
  return None
183
190
 
184
191
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import argparse
4
4
  import getpass
5
+ import logging
5
6
  import os
6
7
  import shutil
7
8
  import sys
@@ -12,6 +13,8 @@ from dulwich import porcelain
12
13
 
13
14
  from github_rest_api import Organization, User
14
15
 
16
+ logger = logging.getLogger(__name__)
17
+
15
18
 
16
19
  def _validate_repo(repo: str) -> None:
17
20
  parts = repo.split("/")
@@ -28,8 +31,9 @@ def _create_remote_repo(
28
31
  if is_owner_user
29
32
  else Organization(token=token, org=owner)
30
33
  )
34
+ logger.info("Creating remote GitHub repository '%s'...", repo)
31
35
  entity.create_repository(name=r, private=private)
32
- print(f"\nCreated the GitHub repo https://github.com/{repo}.\n")
36
+ logger.info("Created the GitHub repo https://github.com/%s.", repo)
33
37
 
34
38
 
35
39
  def _remote_url(repo: str, protocol: str) -> str:
@@ -41,6 +45,7 @@ def _remote_url(repo: str, protocol: str) -> str:
41
45
  def _ensure_remote(path: Path, repo: str, protocol: str) -> None:
42
46
  try:
43
47
  porcelain.remote_add(path, "origin", _remote_url(repo, protocol))
48
+ logger.info("Added remote 'origin' for %s", repo)
44
49
  except porcelain.RemoteExists:
45
50
  pass
46
51
 
@@ -62,39 +67,57 @@ def _init_local_repo(
62
67
  dir_: str,
63
68
  token: str,
64
69
  protocol: str,
70
+ push: bool,
65
71
  branches: Sequence[str] = ("main",),
66
72
  ) -> None:
73
+ branches = list(dict.fromkeys(branches))
74
+ if not branches:
75
+ raise ValueError("At least one branch must be specified.")
67
76
  repo_name = repo.split("/")[-1]
68
77
  path = Path(dir_) if dir_ else Path(repo_name)
69
78
  path.mkdir(parents=True, exist_ok=True)
70
79
  readme = path / "README.md"
71
80
  if not readme.exists():
72
81
  readme.write_text(f"# {repo_name}\n")
82
+ logger.info("Created %s", readme)
73
83
  if not (path / ".git").exists():
74
84
  porcelain.init(path=path)
85
+ logger.info("Initialized empty Git repository in %s", path)
75
86
  if not porcelain.branch_list(path):
76
87
  initial_branch = _active_branch(path)
77
88
  porcelain.add(repo=path, paths=["README.md"])
89
+ logger.info("Added README.md to staging")
78
90
  porcelain.commit(repo=path, message="first commit")
91
+ logger.info("Created first commit")
79
92
  for branch in branches:
80
93
  if branch != initial_branch:
81
94
  porcelain.branch_create(repo=path, name=branch)
95
+ logger.info("Created branch '%s'", branch)
82
96
  porcelain.checkout(repo=path, target=branches[0])
97
+ logger.info("Checked out branch '%s'", branches[0])
83
98
  if initial_branch not in branches:
84
99
  porcelain.branch_delete(repo=path, name=initial_branch)
100
+ logger.info("Deleted initial branch '%s'", initial_branch)
101
+ else:
102
+ existing_branches = {b.decode() for b in porcelain.branch_list(path)}
103
+ for branch in branches:
104
+ if branch not in existing_branches:
105
+ porcelain.branch_create(repo=path, name=branch)
106
+ logger.info("Created branch '%s' from HEAD", branch)
85
107
  _ensure_remote(path, repo, protocol)
86
- local_branches = {b.decode() for b in porcelain.branch_list(path)}
87
- branches_to_push = [branch for branch in branches if branch in local_branches]
88
- if not branches_to_push:
89
- branches_to_push = [_active_branch(path)]
90
- for branch in branches_to_push:
91
- porcelain.push(
92
- repo=path,
93
- remote_location=_remote_url(repo, "https"),
94
- refspecs=[branch.encode()],
95
- username="x-access-token",
96
- password=token,
97
- )
108
+ if push:
109
+ for branch in branches:
110
+ logger.info("Pushing branch '%s' to remote '%s'...", branch, repo)
111
+ porcelain.push(
112
+ repo=path,
113
+ remote_location=_remote_url(repo, "https"),
114
+ refspecs=[branch.encode()],
115
+ username="x-access-token",
116
+ password=token,
117
+ )
118
+ logger.info("Successfully pushed branch '%s'.", branch)
119
+ else:
120
+ logger.info("Skipping push (pass --push to push branches to the remote).")
98
121
  _add_workflow(path, language)
99
122
 
100
123
 
@@ -106,6 +129,7 @@ def create_github_repo(
106
129
  dir_: str,
107
130
  token: str,
108
131
  protocol: str,
132
+ push: bool,
109
133
  branches: Sequence[str] = ("main",),
110
134
  ) -> None:
111
135
  token = token or os.getenv("GITHUB_TOKEN", "")
@@ -127,6 +151,7 @@ def create_github_repo(
127
151
  token=token,
128
152
  branches=branches,
129
153
  protocol=protocol,
154
+ push=push,
130
155
  )
131
156
 
132
157
 
@@ -138,6 +163,7 @@ def _add_workflow(path: Path, language: str, workflow_dir: Path | None = None) -
138
163
  for yaml in workflow_dir.glob("*.yaml"):
139
164
  if not (dir_dest / yaml.name).exists():
140
165
  shutil.copy2(yaml, dir_dest)
166
+ logger.info("Copied workflow %s to %s", yaml.name, dir_dest)
141
167
  if not language:
142
168
  return
143
169
  lang_dir = workflow_dir / language
@@ -146,6 +172,9 @@ def _add_workflow(path: Path, language: str, workflow_dir: Path | None = None) -
146
172
  for yaml in lang_dir.glob("*.yaml"):
147
173
  if not (dir_dest / yaml.name).exists():
148
174
  shutil.copy2(yaml, dir_dest)
175
+ logger.info(
176
+ "Copied %s specific workflow %s to %s", language, yaml.name, dir_dest
177
+ )
149
178
 
150
179
 
151
180
  def parse_args(args=None, namespace=None):
@@ -205,7 +234,7 @@ def parse_args(args=None, namespace=None):
205
234
  nargs="+",
206
235
  default=["main"],
207
236
  metavar="BRANCH",
208
- help="Branches to create and push to remote (default: main).",
237
+ help="Branches to create (and push if --push is set) (default: main).",
209
238
  )
210
239
  parser.add_argument(
211
240
  "--https",
@@ -215,11 +244,25 @@ def parse_args(args=None, namespace=None):
215
244
  default="git",
216
245
  help="Use the HTTPS protocol for the remote URL.",
217
246
  )
247
+ parser.add_argument(
248
+ "--push",
249
+ dest="push",
250
+ action="store_true",
251
+ help="Push branches to the remote (by default, nothing is pushed).",
252
+ )
253
+ parser.add_argument(
254
+ "-v",
255
+ "--verbose",
256
+ dest="verbose",
257
+ action="store_true",
258
+ help="Enable verbose output, including tracebacks on errors.",
259
+ )
218
260
  return parser.parse_args(args=args, namespace=namespace)
219
261
 
220
262
 
221
263
  def main() -> int:
222
264
  args = parse_args()
265
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
223
266
  try:
224
267
  create_github_repo(
225
268
  repo=args.repo,
@@ -230,9 +273,10 @@ def main() -> int:
230
273
  token=args.token,
231
274
  branches=args.branches,
232
275
  protocol=args.protocol,
276
+ push=args.push,
233
277
  )
234
278
  except Exception as e:
235
- print(str(e), file=sys.stderr)
279
+ logger.error("Failed to create GitHub repo: %s", e, exc_info=args.verbose)
236
280
  return 1
237
281
  return 0
238
282
 
@@ -0,0 +1,30 @@
1
+ name: Auto Merge PR
2
+ on:
3
+ pull_request_review:
4
+ types:
5
+ - submitted
6
+ issue_comment:
7
+ types:
8
+ - created
9
+ check_suite:
10
+ types:
11
+ - completed
12
+ schedule:
13
+ - cron: '0 * * * *'
14
+ timezone: 'America/Los_Angeles'
15
+ permissions:
16
+ contents: write
17
+ pull-requests: write
18
+ jobs:
19
+ auto_merge_pull_request:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Install uv
23
+ run: |
24
+ curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
25
+ - name: Auto Merge PR
26
+ run: |
27
+ uvx --from github-rest-api@latest auto_merge_pull_request \
28
+ --authors dclong \
29
+ --approvers dclong 'claude[bot]' 'gemini-code-assist[bot]' \
30
+ --token ${{ secrets.ACTIONS_TOKEN }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-rest-api
3
- Version: 0.47.1
3
+ Version: 0.48.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,5 +1,5 @@
1
1
  github_rest_api/__init__.py,sha256=VVpiUHVLbjjJGNN88OmEvCwq4xmBnXv68T8Apk_lwVc,282
2
- github_rest_api/github.py,sha256=iAfhPLH9UGPtScZfcV1TCeY5XfUzAMqenf5FrSNplQc,45146
2
+ github_rest_api/github.py,sha256=tcP2ETyIySFto82S73IZLDaRaRrj9bZjd9wi-VWKUIo,45427
3
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
@@ -14,17 +14,18 @@ github_rest_api/scripts/container/config_container.py,sha256=BtOqgZlQFM9cWZMoHKv
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
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=gI4qt6ncOk2pB9dfZkXW_YSffbdQhWmqWmMYjXwsD44,7104
17
+ github_rest_api/scripts/github/create_github_repo.py,sha256=6WBMcHtY0rn_ZaF_3TN_sX2QFpklE4TPKNSXOPBwmw0,8919
18
18
  github_rest_api/scripts/github/create_pull_request.py,sha256=gXtRRsxzegGxagHb6k3kxtRd3mt4QVwSPZQpsFnzz1Y,3968
19
19
  github_rest_api/scripts/github/release_on_github.py,sha256=9psaIv-GtrHdvE-tkw0yxCMDmW0bPts-db2vUxhoKXo,4368
20
20
  github_rest_api/scripts/github/remove_branch.py,sha256=iKZ4Y_lQlSOIfifC4plqP-B6pEMnFrknGMvV1oLnuLc,3538
21
+ github_rest_api/scripts/github/workflows/auto_merge_pull_request.yaml,sha256=LOCYzTnubAqBbeGunSdnEt453ZLwJ2870YxRlcPJ7GE,768
21
22
  github_rest_api/scripts/github/workflows/create_pr_dev_to_main.yaml,sha256=fuYNQJ_BthRWmFv2J0JZgB_LrBsmEVAc8rZ-Cs_AqrQ,517
22
23
  github_rest_api/scripts/github/workflows/create_pr_to_dev.yaml,sha256=wFekTLVWffPQOguJCMfu8NnxLQ3OFvN5aBmYih8Ag14,548
23
24
  github_rest_api/scripts/github/workflows/create_pr_to_main.yaml,sha256=mgHPlewkCZfRyMH6PYtPlo-fuE7DaD8v_jj2dSc3nPA,531
24
25
  github_rest_api/scripts/github/workflows/remove_branch.yaml,sha256=KDpB76ovrhuRdP0HN5j6Th9gjIQWSDdh8b4b4yAgtoI,544
25
26
  github_rest_api/scripts/github/workflows/python/lint.yaml,sha256=toP2oUdVIKyu_n20Dr14k5cJYxN8rPksfbX4sfflXxc,699
26
27
  github_rest_api/scripts/github/workflows/python/test.yaml,sha256=86MljYEWX1E9kJYmhMCLwu46WNEWvoyO9PwqeRew540,887
27
- github_rest_api-0.47.1.dist-info/METADATA,sha256=b9iysY6lBcuo-RKdZ2j-z7qXusI1uTbDzuBPuqh1rAU,915
28
- github_rest_api-0.47.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
29
- github_rest_api-0.47.1.dist-info/entry_points.txt,sha256=OfbP9GuQC7SyGbymfCYrXEG-TCcKuu31v0EPNvHc1Q4,659
30
- github_rest_api-0.47.1.dist-info/RECORD,,
28
+ github_rest_api-0.48.0.dist-info/METADATA,sha256=GUo2AkySz1q5KewbBDYUPV-2Yvbchmhd4WXyT2zescs,915
29
+ github_rest_api-0.48.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
30
+ github_rest_api-0.48.0.dist-info/entry_points.txt,sha256=OfbP9GuQC7SyGbymfCYrXEG-TCcKuu31v0EPNvHc1Q4,659
31
+ github_rest_api-0.48.0.dist-info/RECORD,,