github-rest-api 0.38.2__tar.gz → 0.39.1__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.
- github_rest_api-0.39.1/.claude/settings.json +9 -0
- {github_rest_api-0.38.2/github_rest_api/actions/github → github_rest_api-0.39.1/.github}/workflows/create_pr_to_main.yml +1 -2
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.github/workflows/remove_branch.yml +1 -2
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/GEMINI.md +3 -3
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/PKG-INFO +1 -1
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/container/build_container_images.py +21 -12
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/container/config_container.py +12 -6
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/container/update_version_containerfile.py +21 -15
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/add_github_repo.py +18 -13
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/create_pull_request.py +5 -3
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/release_on_github.py +96 -102
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/remove_branch.py +9 -3
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/workflows/create_pr_dev_to_main.yml +1 -2
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/workflows/create_pr_to_dev.yml +1 -2
- {github_rest_api-0.38.2/.github → github_rest_api-0.39.1/github_rest_api/scripts/github}/workflows/create_pr_to_main.yml +1 -2
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/workflows/remove_branch.yml +1 -2
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/utils.py +68 -1
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/pyproject.toml +8 -8
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/tests/test_build_container_images.py +6 -6
- github_rest_api-0.39.1/tests/test_release_on_github.py +62 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/uv.lock +1 -1
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.gemini/system.md +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.github/workflows/lint.yml +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.github/workflows/release.yml +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.github/workflows/test.yml +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.gitignore +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/.gitpod.yml +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/README.md +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/github_rest_api/__init__.py +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/github_rest_api/github.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/__init__.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/cargo/__init__.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/cargo/benchmark.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/cargo/profiling.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/cargo/utils.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/container/__init__.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/__init__.py +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/workflows/python/lint.yml +0 -0
- {github_rest_api-0.38.2/github_rest_api/actions → github_rest_api-0.39.1/github_rest_api/scripts}/github/workflows/python/test.yml +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/github_rest_api/utils.py +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/memory/MEMORY.md +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/memory/feedback_test_runner.md +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/tests/__init__.py +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/tests/test_github.py +0 -0
- {github_rest_api-0.38.2 → github_rest_api-0.39.1}/tests/test_utils.py +0 -0
|
@@ -13,8 +13,7 @@ jobs:
|
|
|
13
13
|
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
14
14
|
- name: Create PR To main
|
|
15
15
|
run: |
|
|
16
|
-
|
|
17
|
-
| uv run --script - \
|
|
16
|
+
uvx --from github-rest-api@latest create_pull_request \
|
|
18
17
|
--head-branch ${{ github.ref_name }} \
|
|
19
18
|
--base-branch main \
|
|
20
19
|
--token ${{ secrets.GITHUBACTIONS }}
|
|
@@ -12,8 +12,7 @@ jobs:
|
|
|
12
12
|
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
13
13
|
- name: Remove Branches
|
|
14
14
|
run: |
|
|
15
|
-
|
|
16
|
-
| uv run --script - \
|
|
15
|
+
uvx --from github-rest-api@latest remove_branch \
|
|
17
16
|
--repo ${{ github.repository }} \
|
|
18
17
|
--pattern '^(?!main$)' \
|
|
19
18
|
--token ${{ secrets.GITHUBACTIONS }}
|
|
@@ -15,9 +15,9 @@ A simple Python wrapper for GitHub REST APIs, optimized for use in GitHub Action
|
|
|
15
15
|
- **psutil**: For system and process utilities.
|
|
16
16
|
- **Architecture:**
|
|
17
17
|
- `github_rest_api/github.py`: Contains the `GitHub` class for handling API requests (GET, POST, DELETE, PUT, PATCH).
|
|
18
|
-
- `github_rest_api/
|
|
19
|
-
- `github_rest_api/
|
|
20
|
-
- `github_rest_api/
|
|
18
|
+
- `github_rest_api/scripts/github/`: Utilities for GitHub actions like creating pull requests, managing releases, and adding repositories.
|
|
19
|
+
- `github_rest_api/scripts/container/`: Utilities for building and configuring container images.
|
|
20
|
+
- `github_rest_api/scripts/cargo/`: Specific support for Rust projects (benchmarking and profiling).
|
|
21
21
|
- `github_rest_api/utils.py`: General-purpose utilities (versioning, partitioning).
|
|
22
22
|
|
|
23
23
|
## Building and Running
|
|
@@ -135,7 +135,9 @@ def _build_image(
|
|
|
135
135
|
def _validate_paths_exist(paths: Sequence[str], label: str) -> None:
|
|
136
136
|
missing = [p for p in paths if not Path(p).exists()]
|
|
137
137
|
if missing:
|
|
138
|
-
|
|
138
|
+
raise FileNotFoundError(
|
|
139
|
+
f"\nError: the following {label} do not exist:\n{'\n'.join(missing)}"
|
|
140
|
+
)
|
|
139
141
|
|
|
140
142
|
|
|
141
143
|
def build_images(
|
|
@@ -168,7 +170,9 @@ def build_images(
|
|
|
168
170
|
print(f"Error building {image_dir}: {e}", flush=True)
|
|
169
171
|
failures.append(image_dir)
|
|
170
172
|
if failures:
|
|
171
|
-
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
f"\n\nError: failed to build images: {', '.join(failures)}\n"
|
|
175
|
+
)
|
|
172
176
|
|
|
173
177
|
|
|
174
178
|
def parse_args():
|
|
@@ -251,17 +255,22 @@ def _resolve_image_dirs(args: argparse.Namespace) -> list[str]:
|
|
|
251
255
|
return [s for item in data if (s := item.strip())]
|
|
252
256
|
|
|
253
257
|
|
|
254
|
-
def main():
|
|
258
|
+
def main() -> int:
|
|
255
259
|
args = parse_args()
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
try:
|
|
261
|
+
build_images(
|
|
262
|
+
args.commit1,
|
|
263
|
+
args.commit2,
|
|
264
|
+
_resolve_image_dirs(args),
|
|
265
|
+
paths_monitoring=args.paths_monitoring,
|
|
266
|
+
tool=args.tool,
|
|
267
|
+
registry=args.registry,
|
|
268
|
+
)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
print(str(e), file=sys.stderr)
|
|
271
|
+
return 1
|
|
272
|
+
return 0
|
|
264
273
|
|
|
265
274
|
|
|
266
275
|
if __name__ == "__main__":
|
|
267
|
-
main()
|
|
276
|
+
sys.exit(main())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
3
|
import shutil
|
|
4
|
+
import sys
|
|
4
5
|
import tomllib
|
|
5
6
|
import tomli_w
|
|
6
7
|
from pathlib import Path
|
|
@@ -69,14 +70,19 @@ def parse_args(args=None):
|
|
|
69
70
|
return parser.parse_args(args=args)
|
|
70
71
|
|
|
71
72
|
|
|
72
|
-
def main():
|
|
73
|
+
def main() -> int:
|
|
73
74
|
args = parse_args()
|
|
74
75
|
runtimes = set(args.runtime) if args.runtime else {"docker", "podman"}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
try:
|
|
77
|
+
if "docker" in runtimes:
|
|
78
|
+
config_docker(args.docker_data_root)
|
|
79
|
+
if "podman" in runtimes:
|
|
80
|
+
config_podman(args.podman_graphroot)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
return 0
|
|
79
85
|
|
|
80
86
|
|
|
81
87
|
if __name__ == "__main__":
|
|
82
|
-
main()
|
|
88
|
+
sys.exit(main())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import datetime
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
import re
|
|
6
7
|
from dulwich import porcelain
|
|
@@ -124,22 +125,27 @@ def checkout_branch(repo: str):
|
|
|
124
125
|
porcelain.checkout(repo=".", target=branch)
|
|
125
126
|
|
|
126
127
|
|
|
127
|
-
def main():
|
|
128
|
+
def main() -> int:
|
|
128
129
|
args = parse_args()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
130
|
+
try:
|
|
131
|
+
if has_open_pr(head_prefix=_branch_prefix(args.repo)):
|
|
132
|
+
return 0
|
|
133
|
+
checkout_branch(args.repo)
|
|
134
|
+
version = parse_latest_version(repo=args.repo)
|
|
135
|
+
if args.next_minor_or_strip_patch is not None:
|
|
136
|
+
version = next_minor_or_strip_patch(version, args.next_minor_or_strip_patch)
|
|
137
|
+
update_version(
|
|
138
|
+
containerfile=args.containerfile,
|
|
139
|
+
version=version,
|
|
140
|
+
pattern=args.pattern,
|
|
141
|
+
replace=args.replace,
|
|
142
|
+
)
|
|
143
|
+
push_changes(repo=args.repo, token=args.token)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print(str(e), file=sys.stderr)
|
|
146
|
+
return 1
|
|
147
|
+
return 0
|
|
142
148
|
|
|
143
149
|
|
|
144
150
|
if __name__ == "__main__":
|
|
145
|
-
main()
|
|
151
|
+
sys.exit(main())
|
|
@@ -77,7 +77,7 @@ def parse_args(args=None, namespace=None):
|
|
|
77
77
|
def _validate_repo(repo: str) -> None:
|
|
78
78
|
parts = repo.split("/")
|
|
79
79
|
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
80
|
-
|
|
80
|
+
raise ValueError(f"Invalid repo format '{repo}'. Expected 'owner/repo'.")
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def _create_remote_repo(
|
|
@@ -142,7 +142,7 @@ def add_github_repo(
|
|
|
142
142
|
if not token:
|
|
143
143
|
token = getpass.getpass("Please enter your GitHub token: ")
|
|
144
144
|
if not token:
|
|
145
|
-
|
|
145
|
+
raise ValueError(
|
|
146
146
|
"No GitHub token is provided (via $GITHUB_TOKEN, --token or at prompt)."
|
|
147
147
|
)
|
|
148
148
|
repo = repo.strip()
|
|
@@ -177,18 +177,23 @@ def _add_workflow(path: Path, language: str, workflow_dir: Path | None = None) -
|
|
|
177
177
|
shutil.copy2(yaml, dir_dest)
|
|
178
178
|
|
|
179
179
|
|
|
180
|
-
def main():
|
|
180
|
+
def main() -> int:
|
|
181
181
|
args = parse_args()
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
try:
|
|
183
|
+
add_github_repo(
|
|
184
|
+
repo=args.repo,
|
|
185
|
+
private=args.private,
|
|
186
|
+
language=args.language,
|
|
187
|
+
is_owner_user=args.is_owner_user,
|
|
188
|
+
dir_=args.dir,
|
|
189
|
+
token=args.token,
|
|
190
|
+
branches=args.branches,
|
|
191
|
+
)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(str(e), file=sys.stderr)
|
|
194
|
+
return 1
|
|
195
|
+
return 0
|
|
191
196
|
|
|
192
197
|
|
|
193
198
|
if __name__ == "__main__":
|
|
194
|
-
main()
|
|
199
|
+
sys.exit(main())
|
|
@@ -4,6 +4,7 @@ The branch is updated (using dev) before creating the PR.
|
|
|
4
4
|
|
|
5
5
|
from argparse import ArgumentParser, Namespace
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
7
8
|
from github_rest_api import Repository
|
|
8
9
|
|
|
9
10
|
|
|
@@ -36,7 +37,7 @@ def parse_args(args=None, namespace=None) -> Namespace:
|
|
|
36
37
|
return parser.parse_args(args=args, namespace=namespace)
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def main():
|
|
40
|
+
def main() -> int:
|
|
40
41
|
"""Main entrance of the script,
|
|
41
42
|
which creates a PR from the specified branch to dev.
|
|
42
43
|
The branch is updated (using dev) before creating the PR.
|
|
@@ -44,7 +45,7 @@ def main():
|
|
|
44
45
|
args = parse_args()
|
|
45
46
|
# skip branches with the pattern _*
|
|
46
47
|
if args.head_branch.startswith("_"):
|
|
47
|
-
return
|
|
48
|
+
return 0
|
|
48
49
|
repo = Repository(args.token, os.environ["GITHUB_REPOSITORY"])
|
|
49
50
|
repo.create_pull_request(
|
|
50
51
|
{
|
|
@@ -53,7 +54,8 @@ def main():
|
|
|
53
54
|
"title": f"Merge {args.head_branch} Into {args.base_branch}",
|
|
54
55
|
},
|
|
55
56
|
)
|
|
57
|
+
return 0
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
if __name__ == "__main__":
|
|
59
|
-
main()
|
|
61
|
+
sys.exit(main())
|
|
@@ -1,93 +1,46 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import sys
|
|
3
4
|
import argparse
|
|
4
|
-
import tomllib
|
|
5
5
|
import getpass
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from dulwich.repo import Repo
|
|
8
|
-
from dulwich.errors import NotGitRepository
|
|
9
7
|
from github_rest_api import Repository
|
|
8
|
+
from github_rest_api.scripts.utils import (
|
|
9
|
+
find_project_root,
|
|
10
|
+
get_project_version,
|
|
11
|
+
get_repo,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_version(root: Path) -> str:
|
|
23
|
-
pyproject = root / "pyproject.toml"
|
|
24
|
-
if pyproject.exists():
|
|
25
|
-
with pyproject.open("rb") as f:
|
|
26
|
-
data = tomllib.load(f)
|
|
27
|
-
return data.get("project", {}).get("version", "")
|
|
28
|
-
cargo = root / "Cargo.toml"
|
|
29
|
-
if cargo.exists():
|
|
30
|
-
with cargo.open("rb") as f:
|
|
31
|
-
data = tomllib.load(f)
|
|
32
|
-
return data.get("package", {}).get("version", "")
|
|
33
|
-
return ""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_repo(root: Path) -> str | None:
|
|
37
|
-
pyproject = root / "pyproject.toml"
|
|
38
|
-
if pyproject.exists():
|
|
39
|
-
with pyproject.open("rb") as f:
|
|
40
|
-
data = tomllib.load(f)
|
|
41
|
-
repo_url = data.get("project", {}).get("urls", {}).get("Repository")
|
|
42
|
-
if repo_url:
|
|
43
|
-
return (
|
|
44
|
-
repo_url.split("github.com/")[-1].removesuffix(".git").rstrip("/")
|
|
45
|
-
)
|
|
46
|
-
# Fallback to git remote
|
|
47
|
-
try:
|
|
48
|
-
repo = Repo(root)
|
|
49
|
-
config = repo.get_config()
|
|
50
|
-
output = config.get((b"remote", b"origin"), b"url").decode().strip()
|
|
51
|
-
if "github.com" in output:
|
|
52
|
-
if output.startswith("git@"):
|
|
53
|
-
return output.split("github.com:")[-1].replace(".git", "").rstrip("/")
|
|
54
|
-
return output.split("github.com/")[-1].replace(".git", "").rstrip("/")
|
|
55
|
-
except (NotGitRepository, KeyError):
|
|
56
|
-
pass
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def parse_args(args=None, namespace=None):
|
|
61
|
-
parser = argparse.ArgumentParser(
|
|
62
|
-
description="Make a release of the project on GitHub."
|
|
63
|
-
)
|
|
64
|
-
parser.add_argument(
|
|
65
|
-
"-b",
|
|
66
|
-
"--branch",
|
|
67
|
-
default="main",
|
|
68
|
-
help="The branch (default to main) from which to make the release.",
|
|
69
|
-
)
|
|
70
|
-
parser.add_argument(
|
|
71
|
-
"-t",
|
|
72
|
-
"--tag",
|
|
73
|
-
default="",
|
|
74
|
-
help="The tag for the release. If not specified, the version from project configuration is used.",
|
|
75
|
-
)
|
|
76
|
-
parser.add_argument(
|
|
77
|
-
"-n",
|
|
78
|
-
"--notes",
|
|
79
|
-
default="",
|
|
80
|
-
help="Notes for the release. If not specified, it's auto generated.",
|
|
81
|
-
)
|
|
82
|
-
parser.add_argument(
|
|
83
|
-
"--token",
|
|
84
|
-
default="",
|
|
85
|
-
help="GitHub token. If not specified, the GITHUB_TOKEN environment variable is used.",
|
|
86
|
-
)
|
|
87
|
-
return parser.parse_args(args=args, namespace=namespace)
|
|
88
|
-
|
|
15
|
+
def _get_release_tag(tag: str, root: Path, validate: bool = True) -> str:
|
|
16
|
+
tag = tag.strip()
|
|
17
|
+
if not tag:
|
|
18
|
+
tag = get_project_version(root).strip()
|
|
19
|
+
if not tag:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"Could not find project version to use as tag. Please specify a tag."
|
|
22
|
+
)
|
|
89
23
|
|
|
90
|
-
|
|
24
|
+
if not validate:
|
|
25
|
+
return tag
|
|
26
|
+
|
|
27
|
+
# matches v?X.Y.Z and optionally pre-releases (e.g., v1.2.3-alpha.1) or build metadata
|
|
28
|
+
semver_pattern = r"v?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?"
|
|
29
|
+
if not re.fullmatch(semver_pattern, tag):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Tag '{
|
|
32
|
+
tag
|
|
33
|
+
}' is not in a semantic versioning format. Use --no-validate to skip."
|
|
34
|
+
)
|
|
35
|
+
normalized = tag if tag.startswith("v") else f"v{tag}"
|
|
36
|
+
if normalized != tag:
|
|
37
|
+
print(f"Tag normalized from '{tag}' to '{normalized}'.")
|
|
38
|
+
return normalized
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def release_on_github(
|
|
42
|
+
token: str, branch: str, tag: str = "", notes: str = "", validate: bool = True
|
|
43
|
+
) -> None:
|
|
91
44
|
"""Make a release of the project on GitHub.
|
|
92
45
|
|
|
93
46
|
The command should be run under the root or a sub-dir of the project.
|
|
@@ -99,29 +52,25 @@ def release_on_github(token: str, branch: str, tag: str = "", notes: str = "") -
|
|
|
99
52
|
If not specified, it's auto generated.
|
|
100
53
|
:param token: GitHub token.
|
|
101
54
|
If not specified, the GITHUB_TOKEN environment variable is used.
|
|
55
|
+
:param validate: If True, validate the tag against semantic versioning format before creating the release.
|
|
102
56
|
"""
|
|
103
|
-
|
|
104
|
-
if not token:
|
|
105
|
-
token = getpass.getpass("Please enter your GitHub token: ")
|
|
106
|
-
if not token:
|
|
107
|
-
sys.exit(
|
|
108
|
-
"No GitHub token is provided (via $GITHUB_TOKEN, --token or at prompt)."
|
|
109
|
-
)
|
|
110
|
-
root = find_root()
|
|
57
|
+
root = find_project_root()
|
|
111
58
|
if not root:
|
|
112
|
-
|
|
59
|
+
raise FileNotFoundError("Could not find project root (no .git found).")
|
|
113
60
|
|
|
114
|
-
|
|
115
|
-
tag = get_version(root)
|
|
116
|
-
if not tag:
|
|
117
|
-
sys.exit(
|
|
118
|
-
"Could not find project version to use as tag. Please specify a tag."
|
|
119
|
-
)
|
|
61
|
+
tag = _get_release_tag(tag, root, validate=validate)
|
|
120
62
|
|
|
121
63
|
repo_name = get_repo(root)
|
|
122
64
|
if not repo_name:
|
|
123
|
-
|
|
65
|
+
raise ValueError("Could not find GitHub repository name.")
|
|
124
66
|
|
|
67
|
+
token = token or os.getenv("GITHUB_TOKEN", "")
|
|
68
|
+
if not token:
|
|
69
|
+
token = getpass.getpass("Please enter your GitHub token: ")
|
|
70
|
+
if not token:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"No GitHub token is provided (via $GITHUB_TOKEN, --token or at prompt)."
|
|
73
|
+
)
|
|
125
74
|
repo = Repository(token=token, repo=repo_name)
|
|
126
75
|
data = {
|
|
127
76
|
"tag_name": tag,
|
|
@@ -136,12 +85,57 @@ def release_on_github(token: str, branch: str, tag: str = "", notes: str = "") -
|
|
|
136
85
|
print(f"Successfully created release {tag} on {repo_name}.")
|
|
137
86
|
|
|
138
87
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
token=args.token, branch=args.branch, tag=args.tag, notes=args.notes
|
|
88
|
+
def parse_args(args=None, namespace=None):
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
description="Make a release of the project on GitHub."
|
|
143
91
|
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"-b",
|
|
94
|
+
"--branch",
|
|
95
|
+
default="main",
|
|
96
|
+
help="The branch (default to main) from which to make the release.",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"-t",
|
|
100
|
+
"--tag",
|
|
101
|
+
default="",
|
|
102
|
+
help="The tag for the release. If not specified, the version from project configuration is used.",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"-n",
|
|
106
|
+
"--notes",
|
|
107
|
+
default="",
|
|
108
|
+
help="Notes for the release. If not specified, it's auto generated.",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--token",
|
|
112
|
+
default="",
|
|
113
|
+
help="GitHub token. If not specified, the GITHUB_TOKEN environment variable is used.",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--no-validate",
|
|
117
|
+
dest="validate",
|
|
118
|
+
action="store_false",
|
|
119
|
+
help="Skip release tag format validation and normalization. Use it exactly as provided.",
|
|
120
|
+
)
|
|
121
|
+
return parser.parse_args(args=args, namespace=namespace)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
args = parse_args()
|
|
126
|
+
try:
|
|
127
|
+
release_on_github(
|
|
128
|
+
token=args.token,
|
|
129
|
+
branch=args.branch,
|
|
130
|
+
tag=args.tag,
|
|
131
|
+
notes=args.notes,
|
|
132
|
+
validate=args.validate,
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(str(e), file=sys.stderr)
|
|
136
|
+
return 1
|
|
137
|
+
return 0
|
|
144
138
|
|
|
145
139
|
|
|
146
140
|
if __name__ == "__main__":
|
|
147
|
-
main()
|
|
141
|
+
sys.exit(main())
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import re
|
|
3
|
+
import sys
|
|
3
4
|
import datetime
|
|
4
5
|
from github_rest_api import Repository
|
|
5
6
|
|
|
@@ -93,10 +94,15 @@ def remove_branch(token: str, repo: str, pattern: str) -> None:
|
|
|
93
94
|
repository.delete_branch(branch_name)
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
def main():
|
|
97
|
+
def main() -> int:
|
|
97
98
|
args = parse_args()
|
|
98
|
-
|
|
99
|
+
try:
|
|
100
|
+
remove_branch(token=args.token, repo=args.repo, pattern=args.pattern)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(str(e), file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
return 0
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
if __name__ == "__main__":
|
|
102
|
-
main()
|
|
108
|
+
sys.exit(main())
|
|
@@ -12,8 +12,7 @@ jobs:
|
|
|
12
12
|
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
13
13
|
- name: Create PR From dev To main
|
|
14
14
|
run: |
|
|
15
|
-
|
|
16
|
-
| uv run --script - \
|
|
15
|
+
uvx --from github-rest-api@latest create_pull_request \
|
|
17
16
|
--head-branch dev \
|
|
18
17
|
--base-branch main \
|
|
19
18
|
--token ${{ secrets.GITHUBACTIONS }}
|
|
@@ -14,8 +14,7 @@ jobs:
|
|
|
14
14
|
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
15
15
|
- name: Create PR to dev
|
|
16
16
|
run: |
|
|
17
|
-
|
|
18
|
-
| uv run --script - \
|
|
17
|
+
uvx --from github-rest-api@latest create_pull_request \
|
|
19
18
|
--head-branch ${{ github.ref_name }} \
|
|
20
19
|
--base-branch dev \
|
|
21
20
|
--token ${{ secrets.GITHUBACTIONS }}
|
|
@@ -13,8 +13,7 @@ jobs:
|
|
|
13
13
|
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
14
14
|
- name: Create PR To main
|
|
15
15
|
run: |
|
|
16
|
-
|
|
17
|
-
| uv run --script - \
|
|
16
|
+
uvx --from github-rest-api@latest create_pull_request \
|
|
18
17
|
--head-branch ${{ github.ref_name }} \
|
|
19
18
|
--base-branch main \
|
|
20
19
|
--token ${{ secrets.GITHUBACTIONS }}
|
|
@@ -12,8 +12,7 @@ jobs:
|
|
|
12
12
|
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
13
13
|
- name: Remove Branches
|
|
14
14
|
run: |
|
|
15
|
-
|
|
16
|
-
| uv run --script - \
|
|
15
|
+
uvx --from github-rest-api@latest remove_branch \
|
|
17
16
|
--repo ${{ github.repository }} \
|
|
18
17
|
--pattern '^(?!(main|dev)$)' \
|
|
19
18
|
--token ${{ secrets.GITHUBACTIONS }}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Util functions for GitHub actions."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import tomllib
|
|
4
|
+
from typing import Any, Iterable
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from collections.abc import Sequence
|
|
5
7
|
import random
|
|
6
8
|
from dulwich import porcelain
|
|
7
9
|
from dulwich.repo import Repo
|
|
@@ -73,3 +75,68 @@ def commit_profiling(prof_dir: str | Path):
|
|
|
73
75
|
"""
|
|
74
76
|
porcelain.add(paths=prof_dir)
|
|
75
77
|
porcelain.commit(message="Updating profiling results.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def find_project_root(path: Path | None = None) -> Path | None:
|
|
81
|
+
if path is None:
|
|
82
|
+
path = Path.cwd()
|
|
83
|
+
while path != path.parent:
|
|
84
|
+
if (path / ".git").exists():
|
|
85
|
+
return path
|
|
86
|
+
path = path.parent
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_toml_value(path: Path, keys: Sequence[str]) -> Any:
|
|
91
|
+
if not keys or not path.exists():
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
with path.open("rb") as f:
|
|
95
|
+
data = tomllib.load(f)
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
for key in keys[:-1]:
|
|
99
|
+
data = data.get(key, {}) if isinstance(data, dict) else {}
|
|
100
|
+
return data.get(keys[-1]) if isinstance(data, dict) else None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_project_version(root: Path) -> str:
|
|
104
|
+
for path, keys in [
|
|
105
|
+
(root / "pyproject.toml", ["project", "version"]),
|
|
106
|
+
(root / "Cargo.toml", ["package", "version"]),
|
|
107
|
+
]:
|
|
108
|
+
version = get_toml_value(path, keys)
|
|
109
|
+
if isinstance(version, str) and version.strip():
|
|
110
|
+
return version
|
|
111
|
+
return ""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_github_repo(url: str) -> str:
|
|
115
|
+
delimiters = ["github.com/", "github.com:"]
|
|
116
|
+
delim = next((d for d in delimiters if d in url), "")
|
|
117
|
+
if not delim:
|
|
118
|
+
return ""
|
|
119
|
+
return url.split(delim)[-1].rstrip("/").removesuffix(".git")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_repo_from_toml(path: Path, keys: Sequence[str]) -> str:
|
|
123
|
+
value = get_toml_value(path, keys)
|
|
124
|
+
return parse_github_repo(value) if isinstance(value, str) else ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_repo(root: Path) -> str | None:
|
|
128
|
+
if repo := _get_repo_from_toml(
|
|
129
|
+
root / "pyproject.toml", ["project", "urls", "Repository"]
|
|
130
|
+
):
|
|
131
|
+
return repo
|
|
132
|
+
if repo := _get_repo_from_toml(root / "Cargo.toml", ["package", "repository"]):
|
|
133
|
+
return repo
|
|
134
|
+
try:
|
|
135
|
+
repo = Repo(root)
|
|
136
|
+
config = repo.get_config()
|
|
137
|
+
url = config.get((b"remote", b"origin"), b"url").decode().strip()
|
|
138
|
+
if repo_name := parse_github_repo(url):
|
|
139
|
+
return repo_name
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
return None
|
|
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "github-rest-api"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.39.1"
|
|
8
8
|
description = "Simple wrapper of GitHub REST APIs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [ { name = "Ben Du", email = "longendu@yahoo.com" } ]
|
|
@@ -23,13 +23,13 @@ dependencies = [
|
|
|
23
23
|
"tenacity>=9.1.4",
|
|
24
24
|
"tomli-w>=1",
|
|
25
25
|
]
|
|
26
|
-
scripts.add_github_repo = "github_rest_api.
|
|
27
|
-
scripts.build_container_images = "github_rest_api.
|
|
28
|
-
scripts.config_container = "github_rest_api.
|
|
29
|
-
scripts.create_pull_request = "github_rest_api.
|
|
30
|
-
scripts.release_on_github = "github_rest_api.
|
|
31
|
-
scripts.remove_branch = "github_rest_api.
|
|
32
|
-
scripts.update_version_containerfile = "github_rest_api.
|
|
26
|
+
scripts.add_github_repo = "github_rest_api.scripts.github.add_github_repo:main"
|
|
27
|
+
scripts.build_container_images = "github_rest_api.scripts.container.build_container_images:main"
|
|
28
|
+
scripts.config_container = "github_rest_api.scripts.container.config_container:main"
|
|
29
|
+
scripts.create_pull_request = "github_rest_api.scripts.github.create_pull_request:main"
|
|
30
|
+
scripts.release_on_github = "github_rest_api.scripts.github.release_on_github:main"
|
|
31
|
+
scripts.remove_branch = "github_rest_api.scripts.github.remove_branch:main"
|
|
32
|
+
scripts.update_version_containerfile = "github_rest_api.scripts.container.update_version_containerfile:main"
|
|
33
33
|
|
|
34
34
|
[dependency-groups]
|
|
35
35
|
dev = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from unittest.mock import patch
|
|
3
|
-
from github_rest_api.
|
|
3
|
+
from github_rest_api.scripts.container.build_container_images import (
|
|
4
4
|
has_relevant_changes,
|
|
5
5
|
)
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ def test_has_relevant_changes_empty_commits():
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@patch(
|
|
22
|
-
"github_rest_api.
|
|
22
|
+
"github_rest_api.scripts.container.build_container_images.changed_files_between",
|
|
23
23
|
return_value=[],
|
|
24
24
|
)
|
|
25
25
|
def test_has_relevant_changes_no_changes(mock_changed):
|
|
@@ -28,7 +28,7 @@ def test_has_relevant_changes_no_changes(mock_changed):
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@patch(
|
|
31
|
-
"github_rest_api.
|
|
31
|
+
"github_rest_api.scripts.container.build_container_images.changed_files_between",
|
|
32
32
|
return_value=[IRRELEVANT_FILE],
|
|
33
33
|
)
|
|
34
34
|
def test_has_relevant_changes_only_irrelevant_files(mock_changed):
|
|
@@ -37,7 +37,7 @@ def test_has_relevant_changes_only_irrelevant_files(mock_changed):
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
@patch(
|
|
40
|
-
"github_rest_api.
|
|
40
|
+
"github_rest_api.scripts.container.build_container_images.changed_files_between",
|
|
41
41
|
return_value=[RELEVANT_FILE],
|
|
42
42
|
)
|
|
43
43
|
def test_has_relevant_changes_with_relevant_file(mock_changed):
|
|
@@ -46,7 +46,7 @@ def test_has_relevant_changes_with_relevant_file(mock_changed):
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@patch(
|
|
49
|
-
"github_rest_api.
|
|
49
|
+
"github_rest_api.scripts.container.build_container_images.changed_files_between",
|
|
50
50
|
return_value=[IRRELEVANT_FILE, RELEVANT_FILE],
|
|
51
51
|
)
|
|
52
52
|
def test_has_relevant_changes_mixed_files(mock_changed):
|
|
@@ -55,7 +55,7 @@ def test_has_relevant_changes_mixed_files(mock_changed):
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
@patch(
|
|
58
|
-
"github_rest_api.
|
|
58
|
+
"github_rest_api.scripts.container.build_container_images.changed_files_between",
|
|
59
59
|
return_value=[RELEVANT_FILE],
|
|
60
60
|
)
|
|
61
61
|
def test_has_relevant_changes_bytes_commits(mock_changed):
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
import pytest
|
|
4
|
+
from github_rest_api.scripts.github.release_on_github import _get_release_tag
|
|
5
|
+
|
|
6
|
+
ROOT = Path(".")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_tag_already_normalized():
|
|
10
|
+
assert _get_release_tag("v1.2.3", ROOT) == "v1.2.3"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_tag_bare_gets_v_prefix(capsys):
|
|
14
|
+
assert _get_release_tag("1.2.3", ROOT) == "v1.2.3"
|
|
15
|
+
assert "normalized" in capsys.readouterr().out
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_tag_with_surrounding_whitespace():
|
|
19
|
+
assert _get_release_tag(" v1.2.3 ", ROOT) == "v1.2.3"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_empty_tag_falls_back_to_version():
|
|
23
|
+
with patch(
|
|
24
|
+
"github_rest_api.scripts.github.release_on_github.get_project_version",
|
|
25
|
+
return_value="1.2.3",
|
|
26
|
+
):
|
|
27
|
+
assert _get_release_tag("", ROOT) == "v1.2.3"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_whitespace_tag_falls_back_to_version():
|
|
31
|
+
with patch(
|
|
32
|
+
"github_rest_api.scripts.github.release_on_github.get_project_version",
|
|
33
|
+
return_value="1.2.3",
|
|
34
|
+
):
|
|
35
|
+
assert _get_release_tag(" ", ROOT) == "v1.2.3"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_empty_tag_no_version_exits():
|
|
39
|
+
with patch(
|
|
40
|
+
"github_rest_api.scripts.github.release_on_github.get_project_version",
|
|
41
|
+
return_value="",
|
|
42
|
+
):
|
|
43
|
+
with pytest.raises(ValueError):
|
|
44
|
+
_get_release_tag("", ROOT)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_invalid_tag_format_exits():
|
|
48
|
+
with pytest.raises(ValueError):
|
|
49
|
+
_get_release_tag("invalid", ROOT)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_no_validate_skips_validation_and_normalization():
|
|
53
|
+
assert _get_release_tag("invalid", ROOT, validate=False) == "invalid"
|
|
54
|
+
assert _get_release_tag("1.2.3", ROOT, validate=False) == "1.2.3"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_prerelease_tag():
|
|
58
|
+
assert _get_release_tag("v1.2.3-alpha.1", ROOT) == "v1.2.3-alpha.1"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_build_metadata_tag():
|
|
62
|
+
assert _get_release_tag("1.2.3+build", ROOT) == "v1.2.3+build"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|