github-rest-api 0.34.1__tar.gz → 0.35.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.
Files changed (44) hide show
  1. github_rest_api-0.35.0/.github/workflows/create_pr_to_main.yml +21 -0
  2. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/.github/workflows/lint.yml +3 -4
  3. github_rest_api-0.35.0/.github/workflows/remove_branch.yml +19 -0
  4. github_rest_api-0.35.0/.github/workflows/test.yml +25 -0
  5. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/.gitignore +1 -0
  6. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/GEMINI.md +9 -4
  7. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/PKG-INFO +4 -3
  8. github_rest_api-0.35.0/github_rest_api/actions/container/__init__.py +1 -0
  9. github_rest_api-0.35.0/github_rest_api/actions/container/build_container_images.py +232 -0
  10. github_rest_api-0.35.0/github_rest_api/actions/container/config_container.py +82 -0
  11. github_rest_api-0.35.0/github_rest_api/actions/container/update_version_containerfile.py +201 -0
  12. github_rest_api-0.35.0/github_rest_api/actions/github/__init__.py +1 -0
  13. github_rest_api-0.35.0/github_rest_api/actions/github/add_github_repo.py +194 -0
  14. github_rest_api-0.35.0/github_rest_api/actions/github/create_pull_request.py +59 -0
  15. github_rest_api-0.35.0/github_rest_api/actions/github/release_on_github.py +147 -0
  16. github_rest_api-0.35.0/github_rest_api/actions/github/remove_branch.py +102 -0
  17. {github_rest_api-0.34.1/.github → github_rest_api-0.35.0/github_rest_api/actions/github}/workflows/create_pr_to_dev.yml +2 -3
  18. github_rest_api-0.35.0/github_rest_api/actions/github/workflows/create_pr_to_main.yml +21 -0
  19. github_rest_api-0.35.0/github_rest_api/actions/github/workflows/python/lint.yml +27 -0
  20. github_rest_api-0.35.0/github_rest_api/actions/github/workflows/python/test.yml +31 -0
  21. github_rest_api-0.35.0/github_rest_api/actions/github/workflows/remove_branch.yml +19 -0
  22. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/github.py +20 -17
  23. github_rest_api-0.35.0/memory/MEMORY.md +3 -0
  24. github_rest_api-0.35.0/memory/feedback_test_runner.md +12 -0
  25. github_rest_api-0.35.0/pyproject.toml +41 -0
  26. github_rest_api-0.35.0/tests/test_build_container_images.py +64 -0
  27. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/uv.lock +24 -43
  28. github_rest_api-0.34.1/pyproject.toml +0 -33
  29. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/.gemini/system.md +0 -0
  30. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/.github/workflows/release.yml +0 -0
  31. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/.gitpod.yml +0 -0
  32. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/README.md +0 -0
  33. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/__init__.py +0 -0
  34. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/actions/__init__.py +0 -0
  35. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/__init__.py +0 -0
  36. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/benchmark.py +0 -0
  37. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/profiling.py +0 -0
  38. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/utils.py +0 -0
  39. {github_rest_api-0.34.1/.github → github_rest_api-0.35.0/github_rest_api/actions/github}/workflows/create_pr_dev_to_main.yml +0 -0
  40. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/actions/utils.py +0 -0
  41. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/github_rest_api/utils.py +0 -0
  42. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/tests/__init__.py +0 -0
  43. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/tests/test_github.py +0 -0
  44. {github_rest_api-0.34.1 → github_rest_api-0.35.0}/tests/test_utils.py +0 -0
@@ -0,0 +1,21 @@
1
+ name: Create PR To main
2
+ on:
3
+ push:
4
+ branches-ignore:
5
+ - main
6
+ - _**
7
+ jobs:
8
+ create_pr_main:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Install uv
12
+ run: |
13
+ curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
14
+ - name: Create PR To main
15
+ run: |
16
+ curl -sSL https://raw.githubusercontent.com/legendu-net/github_actions_scripts/main/create_pull_request.py \
17
+ | uv run --script - \
18
+ --head-branch ${{ github.ref_name }} \
19
+ --base-branch main \
20
+ --token ${{ secrets.GITHUBACTIONS }}
21
+
@@ -1,10 +1,9 @@
1
1
  name: Lint Code
2
2
 
3
3
  on:
4
- push:
5
- branches: [ dev, main ]
6
4
  pull_request:
7
- branches: [ dev ]
5
+ branches:
6
+ - main
8
7
 
9
8
  jobs:
10
9
  lint_code:
@@ -22,4 +21,4 @@ jobs:
22
21
  - name: Lint with Ty
23
22
  run: uv run ty check
24
23
  - name: Analyze Dependencies
25
- run: uv run deptry .
24
+ run: uv run deptry .
@@ -0,0 +1,19 @@
1
+ name: Remove Branches
2
+ on:
3
+ schedule:
4
+ - cron: '0 3 * * *'
5
+ timezone: 'America/Los_Angeles'
6
+ jobs:
7
+ remove_branch:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - name: Install uv
11
+ run: |
12
+ curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
13
+ - name: Remove Branches
14
+ run: |
15
+ curl -sSL https://raw.githubusercontent.com/legendu-net/github_actions_scripts/main/remove_branch.py \
16
+ | uv run --script - \
17
+ --repo ${{ github.repository }} \
18
+ --pattern '^(?!main$)' \
19
+ --token ${{ secrets.GITHUBACTIONS }}
@@ -0,0 +1,25 @@
1
+ name: Run Tests
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - main
6
+ jobs:
7
+ test_code:
8
+ runs-on: ${{matrix.os}}
9
+ strategy:
10
+ matrix:
11
+ os:
12
+ - macOS-latest
13
+ - ubuntu-latest
14
+ python-version:
15
+ - "3.12"
16
+ - "3.13"
17
+ - "3.14"
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ - name: Install dependencies
21
+ run: |
22
+ curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
23
+ uv sync --all-extras
24
+ - name: Test with pytest
25
+ run: uv run --python ${{ matrix.python-version }} pytest
@@ -19,3 +19,4 @@ dist/
19
19
  doc*/_build/
20
20
  *.prof
21
21
  core
22
+ .claude/settings.local.json
@@ -7,13 +7,16 @@ A simple Python wrapper for GitHub REST APIs, optimized for use in GitHub Action
7
7
  - **Purpose:** Provide a streamlined interface for interacting with GitHub's REST API
8
8
  and performing Git operations within automation scripts.
9
9
  - **Main Technologies:**
10
- - **Python 3.11+**: Core language.
10
+ - **Python 3.12+**: Core language.
11
11
  - **requests**: For HTTP interactions with the GitHub API.
12
12
  - **dulwich**: A pure-Python implementation of Git for repository operations.
13
+ - **tenacity**: For retry logic on API requests.
14
+ - **tomli-w**: For writing TOML files.
13
15
  - **psutil**: For system and process utilities.
14
16
  - **Architecture:**
15
17
  - `github_rest_api/github.py`: Contains the `GitHub` class for handling API requests (GET, POST, DELETE, PUT, PATCH).
16
- - `github_rest_api/actions/`: Focused utilities for GitHub Actions, including branch management and pushing changes.
18
+ - `github_rest_api/actions/github/`: Utilities for GitHub actions like creating pull requests, managing releases, and adding repositories.
19
+ - `github_rest_api/actions/container/`: Utilities for building and configuring container images.
17
20
  - `github_rest_api/actions/cargo/`: Specific support for Rust projects (benchmarking and profiling).
18
21
  - `github_rest_api/utils.py`: General-purpose utilities (versioning, partitioning).
19
22
 
@@ -28,6 +31,7 @@ This project uses `uv` for dependency and environment management.
28
31
  - **Code Formatting:**
29
32
  ```bash
30
33
  uv run ruff format ./
34
+ uv run pyproject-fmt pyproject.toml
31
35
  ```
32
36
  - **Linting:**
33
37
  ```bash
@@ -36,6 +40,7 @@ This project uses `uv` for dependency and environment management.
36
40
  - **Type Checking:**
37
41
  ```bash
38
42
  uv run ty check
43
+ uv run pyright
39
44
  ```
40
45
  - **Dependency Analysis:**
41
46
  ```bash
@@ -48,8 +53,8 @@ This project uses `uv` for dependency and environment management.
48
53
 
49
54
  ## Development Conventions
50
55
 
51
- - **Code Style:** Strictly follows `ruff` formatting and linting rules.
52
- - **Type Safety:** Uses `ty` (in addition to standard type hints) to ensure type correctness.
56
+ - **Code Style:** Strictly follows `ruff` formatting and linting rules. `pyproject-fmt` is used for TOML formatting.
57
+ - **Type Safety:** Uses `ty` and `pyright` to ensure type correctness.
53
58
  - **CI/CD:** Automated linting and formatting checks are performed
54
59
  on `push` to `dev`/`main` branches and on `pull_request` to `dev`.
55
60
  - **Git Operations:** Prefers `dulwich` for programmatic Git interactions
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-rest-api
3
- Version: 0.34.1
3
+ Version: 0.35.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
7
- Classifier: Programming Language :: Python :: 3.11
8
7
  Classifier: Programming Language :: Python :: 3.12
9
8
  Classifier: Programming Language :: Python :: 3.13
10
9
  Classifier: Programming Language :: Python :: 3.14
11
- Requires-Python: <4,>=3.11
10
+ Requires-Python: <4,>=3.12
12
11
  Requires-Dist: dulwich>=0.25.1
13
12
  Requires-Dist: psutil>=5.9.4
14
13
  Requires-Dist: requests>=2.28.2
14
+ Requires-Dist: tenacity>=9.1.4
15
+ Requires-Dist: tomli-w>=1
15
16
  Description-Content-Type: text/markdown
16
17
 
17
18
  # GitHub REST APIs | [@GitHub](https://github.com/legendu-net/github_rest_api) | [@PyPI](https://pypi.org/project/github-rest-api/)
@@ -0,0 +1 @@
1
+ """Container-related GitHub Actions scripts."""
@@ -0,0 +1,232 @@
1
+ import argparse
2
+ import datetime
3
+ from pathlib import Path
4
+ import subprocess as sp
5
+ import sys
6
+ from typing import cast
7
+ from dulwich.repo import Repo
8
+ from dulwich.refs import Ref
9
+ from dulwich.objects import Commit
10
+ from dulwich.errors import NotGitRepository
11
+ from dulwich.diff_tree import tree_changes
12
+ from tenacity import retry, stop_after_attempt, wait_exponential
13
+
14
+
15
+ def _get_commit(name: bytes) -> bytes:
16
+ """Resolve a commit SHA or branch name to a commit SHA string."""
17
+ repo = Repo(".")
18
+ if name in repo:
19
+ return name
20
+ for prefix in [b"refs/heads/", b"refs/remotes/origin/", b"refs/tags/"]:
21
+ ref = cast(Ref, prefix + name)
22
+ if ref in repo.refs:
23
+ return repo.refs[ref]
24
+ raise KeyError(f"Cannot resolve commit or branch: {name}")
25
+
26
+
27
+ def changed_files_between(
28
+ commit1: bytes, commit2: bytes, name1: str = "", name2: str = ""
29
+ ) -> list[Path]:
30
+ """Get a unique list of changed files between 2 commits.
31
+
32
+ :param commit1: The first commit ID.
33
+ :param commit2: The second commit ID.
34
+ :param name1: Optional human-readable name for commit1 used in logging (defaults to the commit SHA).
35
+ :param name2: Optional human-readable name for commit2 used in logging (defaults to the commit SHA).
36
+ :return: A unique list of changed files.
37
+ """
38
+ repo = Repo(".")
39
+ c1 = cast(Commit, repo[commit1])
40
+ c2 = cast(Commit, repo[commit2])
41
+ changes = tree_changes(repo.object_store, c1.tree, c2.tree)
42
+ files = set()
43
+ for change in changes:
44
+ if change.old and change.old.path:
45
+ files.add(change.old.path.decode())
46
+ if change.new and change.new.path:
47
+ files.add(change.new.path.decode())
48
+ paths = sorted(Path(file) for file in files)
49
+ print(
50
+ f"Changed files between {name1 or commit1.decode()[:7]} and {
51
+ name2 or commit2.decode()[:7]
52
+ }:"
53
+ )
54
+ for p in paths:
55
+ print(f" {p}")
56
+ return paths
57
+
58
+
59
+ def has_relevant_changes(
60
+ commit1: str | bytes,
61
+ commit2: str | bytes,
62
+ image_dirs: list[str],
63
+ name1: str = "",
64
+ name2: str = "",
65
+ ) -> bool:
66
+ if not commit1 or not commit2:
67
+ return True
68
+ if isinstance(commit1, str):
69
+ commit1 = commit1.encode()
70
+ if isinstance(commit2, str):
71
+ commit2 = commit2.encode()
72
+ dirs = [Path(d).resolve() for d in image_dirs]
73
+ for p in changed_files_between(commit1, commit2, name1=name1, name2=name2):
74
+ if any(p.resolve().is_relative_to(d) for d in dirs):
75
+ return True
76
+ return False
77
+
78
+
79
+ def has_relevant_changes_main_dev(image_dirs: list[str]) -> bool:
80
+ try:
81
+ c_main = _get_commit(b"main")
82
+ c_dev = _get_commit(b"dev")
83
+ except (KeyError, NotGitRepository):
84
+ return True
85
+ return has_relevant_changes(c_main, c_dev, image_dirs, name1="main", name2="dev")
86
+
87
+
88
+ def _tag_date(tag: str) -> str:
89
+ """Suffix a tag with the current date as a 6-digit string.
90
+
91
+ :param tag: A tag of a Podman image.
92
+ :return: A new tag.
93
+ """
94
+ return tag + datetime.datetime.now(tz=datetime.timezone.utc).strftime("_%m%d%H")
95
+
96
+
97
+ @retry(
98
+ stop=stop_after_attempt(3), wait=wait_exponential(multiplier=60, min=60, max=300)
99
+ )
100
+ def _push_image(image: str, tool: str = "podman"):
101
+ sp.run(
102
+ [tool, "push", image],
103
+ shell=False,
104
+ check=True,
105
+ )
106
+
107
+
108
+ def _build_image(
109
+ image_dir: str,
110
+ tags: str | list[str],
111
+ tool: str = "podman",
112
+ registry: str = "quay.io/legendu",
113
+ ):
114
+ if isinstance(tags, str):
115
+ tags = [tags]
116
+ image = f"{registry}/{image_dir}"
117
+ print(f"\n\nBuilding the {tool} image {image}...", flush=True)
118
+ cmd = [tool, "build", image_dir]
119
+ for tag in tags:
120
+ cmd.append("-t")
121
+ cmd.append(f"{image}:{tag}")
122
+ sp.run(cmd, shell=False, check=True)
123
+ for tag in tags:
124
+ _push_image(f"{image}:{tag}", tool=tool)
125
+
126
+
127
+ def build_images(
128
+ commit1: str,
129
+ commit2: str,
130
+ image_dirs: list[str],
131
+ tool: str = "podman",
132
+ registry: str = "quay.io/legendu",
133
+ ):
134
+ if not has_relevant_changes(commit1, commit2, image_dirs):
135
+ print(
136
+ f"Skip building {tool} images as there are no relevant changes between {
137
+ commit1
138
+ } and {commit2}.\n"
139
+ )
140
+ return
141
+ tags = ["next"]
142
+ if not has_relevant_changes_main_dev(image_dirs):
143
+ tags.append("latest")
144
+ tags.extend([_tag_date(tag) for tag in tags])
145
+ print(f"Building {tool} images using tags:", ", ".join(tags), "\n", flush=True)
146
+ failures = []
147
+ for image_dir in image_dirs:
148
+ try:
149
+ _build_image(image_dir, tags=tags, tool=tool, registry=registry)
150
+ except (sp.CalledProcessError, FileNotFoundError) as e:
151
+ print(f"Error building {image_dir}: {e}", flush=True)
152
+ failures.append(image_dir)
153
+ if failures:
154
+ sys.exit(f"\n\nError: failed to build images: {', '.join(failures)}\n")
155
+
156
+
157
+ def parse_args():
158
+ """Parse command-line arguments.
159
+
160
+ :return: An object containing the parsed arguments.
161
+ """
162
+ parser = argparse.ArgumentParser(description="Build container images.")
163
+ parser.add_argument(
164
+ "-c1",
165
+ "--commit1",
166
+ dest="commit1",
167
+ default="",
168
+ help="The first commit ID (empty by default).",
169
+ )
170
+ parser.add_argument(
171
+ "-c2",
172
+ "--commit2",
173
+ dest="commit2",
174
+ default="",
175
+ help="The second commit ID (empty by default).",
176
+ )
177
+ parser.add_argument(
178
+ "-r",
179
+ "--registry",
180
+ dest="registry",
181
+ default="quay.io/legendu",
182
+ help="Container registry prefix (default: quay.io/legendu).",
183
+ )
184
+ parser.add_argument(
185
+ "-t",
186
+ "--tool",
187
+ dest="tool",
188
+ default="podman",
189
+ choices=["podman", "docker"],
190
+ help="Container tool to use for building and pushing images (default: podman).",
191
+ )
192
+ group = parser.add_mutually_exclusive_group(required=True)
193
+ group.add_argument(
194
+ "-i",
195
+ "--image-dirs",
196
+ dest="image_dirs",
197
+ nargs="+",
198
+ default=None,
199
+ metavar="IMAGE_DIR",
200
+ help="Explicit list of image directories to build.",
201
+ )
202
+ group.add_argument(
203
+ "-f",
204
+ "--file-image-dirs",
205
+ dest="file_image_dirs",
206
+ default=None,
207
+ metavar="FILE",
208
+ help="Path to a file listing image directories to build, one per line.",
209
+ )
210
+ return parser.parse_args()
211
+
212
+
213
+ def _resolve_image_dirs(args: argparse.Namespace) -> list[str]:
214
+ if args.image_dirs:
215
+ return args.image_dirs
216
+ lines = Path(args.file_image_dirs).read_text().splitlines()
217
+ return [line.strip() for line in lines if line.strip()]
218
+
219
+
220
+ def main():
221
+ args = parse_args()
222
+ build_images(
223
+ args.commit1,
224
+ args.commit2,
225
+ _resolve_image_dirs(args),
226
+ tool=args.tool,
227
+ registry=args.registry,
228
+ )
229
+
230
+
231
+ if __name__ == "__main__":
232
+ main()
@@ -0,0 +1,82 @@
1
+ import argparse
2
+ import json
3
+ import shutil
4
+ import tomllib
5
+ import tomli_w
6
+ from pathlib import Path
7
+ import subprocess as sp
8
+
9
+
10
+ def config_docker(data_root: str = "/mnt/docker"):
11
+ if not shutil.which("docker"):
12
+ print("Docker not found, skipping Docker configuration.")
13
+ return
14
+ Path(data_root).mkdir(parents=True, exist_ok=True)
15
+ sp.run(["systemctl", "stop", "docker"], check=True)
16
+ path = Path("/etc/docker/daemon.json")
17
+ settings: dict = {}
18
+ if path.is_file() and path.stat().st_size > 0:
19
+ with path.open("r", encoding="utf-8") as fin:
20
+ settings = json.load(fin)
21
+ settings["data-root"] = data_root
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ with path.open("w", encoding="utf-8") as fout:
24
+ json.dump(settings, fout, indent=4)
25
+ print(settings)
26
+ sp.run(["systemctl", "start", "docker"], check=True)
27
+ sp.run(["docker", "info"], check=True)
28
+
29
+
30
+ def config_podman(graphroot: str = "/mnt/podman"):
31
+ if not shutil.which("podman"):
32
+ print("Podman not found, skipping Podman configuration.")
33
+ return
34
+ Path(graphroot).mkdir(parents=True, exist_ok=True)
35
+ path = Path("/etc/containers/storage.conf")
36
+ settings: dict = {}
37
+ if path.is_file() and path.stat().st_size > 0:
38
+ with path.open("rb") as fin:
39
+ settings = tomllib.load(fin)
40
+ storage = settings.setdefault("storage", {})
41
+ storage["graphroot"] = graphroot
42
+ storage.setdefault("driver", "overlay")
43
+ storage.setdefault("runroot", "/run/containers/storage")
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ with path.open("wb") as fout:
46
+ tomli_w.dump(settings, fout)
47
+ print(settings)
48
+ sp.run(["podman", "info"], check=True)
49
+
50
+
51
+ def parse_args(args=None):
52
+ parser = argparse.ArgumentParser(description="Configure container runtimes.")
53
+ parser.add_argument(
54
+ "runtime",
55
+ nargs="*",
56
+ choices=["docker", "podman"],
57
+ help="Container runtime(s) to configure. Configures both if not specified.",
58
+ )
59
+ parser.add_argument(
60
+ "--docker-data-root",
61
+ default="/mnt/docker",
62
+ help="Docker data-root directory (default: /mnt/docker).",
63
+ )
64
+ parser.add_argument(
65
+ "--podman-graphroot",
66
+ default="/mnt/podman",
67
+ help="Podman graphroot directory (default: /mnt/podman).",
68
+ )
69
+ return parser.parse_args(args=args)
70
+
71
+
72
+ def main():
73
+ args = parse_args()
74
+ runtimes = set(args.runtime) if args.runtime else {"docker", "podman"}
75
+ if "docker" in runtimes:
76
+ config_docker(args.docker_data_root)
77
+ if "podman" in runtimes:
78
+ config_podman(args.podman_graphroot)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,201 @@
1
+ import argparse
2
+ import datetime
3
+ import os
4
+ from pathlib import Path
5
+ import re
6
+ from dulwich import porcelain
7
+ from github_rest_api import Repository
8
+ from github_rest_api.utils import next_minor_or_strip_patch
9
+ from requests.exceptions import HTTPError
10
+
11
+
12
+ def parse_latest_version(repo: str) -> str:
13
+ r = Repository(token="", repo=repo)
14
+ try:
15
+ release = r.get_release_latest()
16
+ version = release["tag_name"]
17
+ except HTTPError as err:
18
+ if err.response is not None and err.response.status_code == 404:
19
+ tags = r.get_tags(n=1)
20
+ version = tags[0]["name"]
21
+ else:
22
+ raise err
23
+ version = version.replace("v", "")
24
+ print(f"The latest version of {repo} is v{version}.")
25
+ return version
26
+
27
+
28
+ def update_version(
29
+ containerfile: str | Path, version: str, pattern: str, replace: str
30
+ ) -> None:
31
+ if containerfile == "":
32
+ containerfile = "Dockerfile" if Path("Dockerfile").exists() else "Containerfile"
33
+ if isinstance(containerfile, str):
34
+ containerfile = Path(containerfile).resolve()
35
+ match containerfile.parent.name:
36
+ case "docker-base":
37
+ return _update_version_docker_base(
38
+ containerfile=containerfile, version=version
39
+ )
40
+ case "docker-jupyterlab":
41
+ return _update_version_docker_jupyterlab(
42
+ containerfile=containerfile, version=version
43
+ )
44
+ case "docker-jupyterhub":
45
+ return _update_version_docker_jupyterhub(
46
+ containerfile=containerfile, version=version
47
+ )
48
+ case "docker-vscode-server":
49
+ return _update_version_docker_vscode_server(
50
+ containerfile=containerfile, version=version
51
+ )
52
+ case _:
53
+ if not pattern:
54
+ raise ValueError("A version pattern must be specified!")
55
+ return _update_version_default(
56
+ containerfile=containerfile,
57
+ version=version,
58
+ pattern=pattern,
59
+ replace=replace,
60
+ )
61
+
62
+
63
+ def _update_version_default(
64
+ containerfile: Path, version: str, pattern: str, replace: str
65
+ ) -> None:
66
+ text = containerfile.read_text()
67
+ text = re.sub(pattern, replace.format(version=version), text)
68
+ containerfile.write_text(text)
69
+
70
+
71
+ def _update_version_docker_base(containerfile: Path, version: str) -> None:
72
+ _update_version_default(
73
+ containerfile=containerfile,
74
+ version=version,
75
+ pattern=r"-v v?\d+\.\d+\.\d+",
76
+ replace="-v v{version}",
77
+ )
78
+
79
+
80
+ def _update_version_docker_jupyterlab(containerfile: Path, version: str) -> None:
81
+ version = next_minor_or_strip_patch(version, 3)
82
+ _update_version_default(
83
+ containerfile=containerfile,
84
+ version=version,
85
+ pattern=r",<\d+\.\d+\.0",
86
+ replace=",<{version}",
87
+ )
88
+
89
+
90
+ def _update_version_docker_jupyterhub(containerfile: Path, version: str) -> None:
91
+ version = next_minor_or_strip_patch(version, 3)
92
+ _update_version_default(
93
+ containerfile=containerfile,
94
+ version=version,
95
+ pattern=r"jupyterhub<\d+\.\d+\.0",
96
+ replace="jupyterhub<{version}",
97
+ )
98
+
99
+
100
+ def _update_version_docker_vscode_server(containerfile: Path, version: str) -> None:
101
+ version = next_minor_or_strip_patch(version, 3)
102
+ _update_version_default(
103
+ containerfile=containerfile,
104
+ version=version,
105
+ pattern=r",<\d+\.\d+\.0",
106
+ replace=",<{version}",
107
+ )
108
+
109
+
110
+ def _branch_prefix(repo: str) -> str:
111
+ return repo.replace("/", "_") + "_version"
112
+
113
+
114
+ def push_changes(repo: str, token: str):
115
+ if not porcelain.status().unstaged:
116
+ print("No changes!")
117
+ return
118
+ porcelain.add()
119
+ porcelain.commit(message=f"update version of {repo}")
120
+ gh_repo = os.environ["GITHUB_REPOSITORY"]
121
+ porcelain.push(
122
+ repo=".",
123
+ remote_location=f"https://github.com/{gh_repo}.git",
124
+ username="x-access-token",
125
+ password=token,
126
+ )
127
+
128
+
129
+ def parse_args():
130
+ """Parse command-line arguments."""
131
+ parser = argparse.ArgumentParser(
132
+ description="Update the version of a package in a Dockerfile or Containerfile."
133
+ )
134
+ parser.add_argument(
135
+ "--containerfile",
136
+ dest="containerfile",
137
+ default="",
138
+ help="The Dockerfile or Containerfile to update.",
139
+ )
140
+ parser.add_argument(
141
+ "--token",
142
+ dest="token",
143
+ required=True,
144
+ help="A GitHub token for the repo to be updated.",
145
+ )
146
+ parser.add_argument(
147
+ "--repo",
148
+ dest="repo",
149
+ required=True,
150
+ help="The GitHub repo (in the format of owner/repo) whose release versions are watched.",
151
+ )
152
+ parser.add_argument(
153
+ "--pattern",
154
+ dest="pattern",
155
+ default="",
156
+ help="The version pattern to replace.",
157
+ )
158
+ parser.add_argument(
159
+ "--replace",
160
+ dest="replace",
161
+ default="",
162
+ help="The replacement for the matched version pattern.",
163
+ )
164
+ return parser.parse_args()
165
+
166
+
167
+ def has_open_pr(head_prefix: str) -> bool:
168
+ """Check if there's an open PR whose head starts with head_prefix.
169
+
170
+ :param head_prefix: The prefix of head to check for.
171
+ """
172
+ prs = Repository(token="", repo=os.environ["GITHUB_REPOSITORY"]).get_pull_requests()
173
+ for pr in prs:
174
+ if pr["head"]["ref"].startswith(head_prefix):
175
+ return True
176
+ return False
177
+
178
+
179
+ def checkout_branch(repo: str):
180
+ branch = _branch_prefix(repo) + datetime.date.today().strftime("_%Y%m%d")
181
+ porcelain.branch_create(repo=".", name=branch)
182
+ porcelain.checkout(repo=".", target=branch)
183
+
184
+
185
+ def main():
186
+ args = parse_args()
187
+ if has_open_pr(head_prefix=_branch_prefix(args.repo)):
188
+ return
189
+ checkout_branch(args.repo)
190
+ version = parse_latest_version(repo=args.repo)
191
+ update_version(
192
+ containerfile=args.containerfile,
193
+ version=version,
194
+ pattern=args.pattern,
195
+ replace=args.replace,
196
+ )
197
+ push_changes(repo=args.repo, token=args.token)
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
@@ -0,0 +1 @@
1
+ """GitHub-related Actions scripts."""