github-rest-api 0.34.0__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.
- github_rest_api-0.35.0/.github/workflows/create_pr_to_main.yml +21 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/.github/workflows/lint.yml +3 -4
- github_rest_api-0.35.0/.github/workflows/remove_branch.yml +19 -0
- github_rest_api-0.35.0/.github/workflows/test.yml +25 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/.gitignore +1 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/GEMINI.md +9 -4
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/PKG-INFO +4 -3
- github_rest_api-0.35.0/github_rest_api/actions/container/__init__.py +1 -0
- github_rest_api-0.35.0/github_rest_api/actions/container/build_container_images.py +232 -0
- github_rest_api-0.35.0/github_rest_api/actions/container/config_container.py +82 -0
- github_rest_api-0.35.0/github_rest_api/actions/container/update_version_containerfile.py +201 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/__init__.py +1 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/add_github_repo.py +194 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/create_pull_request.py +59 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/release_on_github.py +147 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/remove_branch.py +102 -0
- {github_rest_api-0.34.0/.github → github_rest_api-0.35.0/github_rest_api/actions/github}/workflows/create_pr_to_dev.yml +2 -3
- github_rest_api-0.35.0/github_rest_api/actions/github/workflows/create_pr_to_main.yml +21 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/workflows/python/lint.yml +27 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/workflows/python/test.yml +31 -0
- github_rest_api-0.35.0/github_rest_api/actions/github/workflows/remove_branch.yml +19 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/github.py +23 -20
- github_rest_api-0.35.0/memory/MEMORY.md +3 -0
- github_rest_api-0.35.0/memory/feedback_test_runner.md +12 -0
- github_rest_api-0.35.0/pyproject.toml +41 -0
- github_rest_api-0.35.0/tests/test_build_container_images.py +64 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/uv.lock +24 -43
- github_rest_api-0.34.0/pyproject.toml +0 -33
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/.gemini/system.md +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/.github/workflows/release.yml +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/.gitpod.yml +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/README.md +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/__init__.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/actions/__init__.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/__init__.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/benchmark.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/profiling.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/actions/cargo/utils.py +0 -0
- {github_rest_api-0.34.0/.github → github_rest_api-0.35.0/github_rest_api/actions/github}/workflows/create_pr_dev_to_main.yml +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/actions/utils.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/github_rest_api/utils.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/tests/__init__.py +0 -0
- {github_rest_api-0.34.0 → github_rest_api-0.35.0}/tests/test_github.py +0 -0
- {github_rest_api-0.34.0 → 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:
|
|
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
|
|
@@ -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.
|
|
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/`:
|
|
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`
|
|
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.
|
|
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.
|
|
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."""
|