pkg-deploy 1.0.12__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.
pkg_deploy/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ __version__ = "1.0.12"
2
+
3
+ from .deploy import PackageDeploy
pkg_deploy/build.py ADDED
@@ -0,0 +1,238 @@
1
+ import os
2
+ import sys
3
+ import copy
4
+ import logging
5
+ import textwrap
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ from dataclasses import dataclass
10
+ from abc import ABC, abstractmethod
11
+ from tomlkit.toml_document import TOMLDocument
12
+
13
+ from .utils import save_config, is_uv_venv, ensure_uv_installed
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class DeployConfig:
21
+ package_name: str
22
+ project_dir: Path
23
+ package_dir: Path
24
+ package_entry: str
25
+ pyproject_path: Path
26
+ version_type: str
27
+ new_version: str
28
+ use_cython: bool
29
+ use_cibuildwheel: bool
30
+ is_uv_venv: bool
31
+ repository_name: str
32
+ repository_url: Optional[str] = None
33
+ username: Optional[str] = None
34
+ password: Optional[str] = None
35
+ dry_run: bool = False
36
+
37
+
38
+ class BuildStrategy(ABC):
39
+
40
+ @staticmethod
41
+ def build_cmd(config: DeployConfig):
42
+ if config.use_cibuildwheel:
43
+ cmd = ['cibuildwheel', '--output-dir', 'dist']
44
+ elif is_uv_venv():
45
+ ensure_uv_installed()
46
+ cmd = ["uv", "build", "--wheel"]
47
+ else:
48
+ cmd = [sys.executable, "-m", "build", "--wheel"]
49
+ return cmd
50
+
51
+ @abstractmethod
52
+ def build(self, config: DeployConfig, toml_config: TOMLDocument) -> bool:
53
+ pass
54
+
55
+
56
+ class StandardBuildStrategy(BuildStrategy):
57
+
58
+ def build(self, config: DeployConfig, toml_config: TOMLDocument) -> bool:
59
+ cmd = self.build_cmd(config=config)
60
+ logger.info(f"Running: {' '.join(cmd)}")
61
+ result = subprocess.run(
62
+ cmd,
63
+ capture_output=True,
64
+ text=True,
65
+ encoding="utf-8",
66
+ cwd=config.project_dir
67
+ )
68
+ if result.returncode != 0:
69
+ raise ValueError(f"Build failed, \nstdout: {result.stdout}\nstderr: {result.stderr}")
70
+ logger.info("Standard build completed successfully")
71
+ return True
72
+
73
+ class CythonBuildStrategy(BuildStrategy):
74
+
75
+ def build(self, config: DeployConfig, toml_config: TOMLDocument) -> bool:
76
+ try:
77
+ self.prepare_pyproject_for_cython_build(config.project_dir, toml_config)
78
+ self.create_setup_py_for_cython(config, toml_config)
79
+ cmd = self.build_cmd(config=config)
80
+ logger.info(f"Running Cython build: {' '.join(cmd)}")
81
+ env = os.environ.copy()
82
+ env['CYTHONIZE'] = '1'
83
+ env['PYTHONIOENCODING'] = 'utf-8'
84
+ env['PYTHONUTF8'] = '1'
85
+ env["CIBW_BUILD_VERBOSITY"] = "1"
86
+ result = subprocess.run(
87
+ cmd,
88
+ capture_output=True,
89
+ text=True,
90
+ encoding="utf-8",
91
+ env=env,
92
+ cwd=config.project_dir
93
+ )
94
+ if result.returncode != 0:
95
+ raise ValueError(f"Cython build failed, \nstdout: {result.stdout}\nstderr: {result.stderr}")
96
+ logger.info("Cython build completed successfully")
97
+ return True
98
+ except Exception as e:
99
+ logger.error(f"Cython build error: {e}")
100
+ return False
101
+ finally:
102
+ if not config.dry_run:
103
+ self.restore_pyproject_toml(project_dir=config.project_dir, original_toml_config=toml_config)
104
+
105
+ @staticmethod
106
+ def prepare_pyproject_for_cython_build(project_dir: Path, toml_config: TOMLDocument):
107
+ pyproject_path = project_dir / "pyproject.toml"
108
+ if pyproject_path.exists():
109
+ new_config = copy.deepcopy(toml_config)
110
+
111
+ # Add Cython build dependency
112
+ if 'build-system' not in new_config:
113
+ new_config['build-system'] = {}
114
+
115
+ if 'requires' not in new_config['build-system']:
116
+ new_config['build-system']['requires'] = []
117
+
118
+ cython_deps = ['setuptools', 'Cython']
119
+ requires = new_config['build-system']['requires']
120
+ for dep in cython_deps:
121
+ if not any(req.startswith(dep) for req in requires):
122
+ new_config['build-system']['requires'].append(dep)
123
+
124
+ if 'build-backend' not in new_config['build-system'] or new_config['build-system'][
125
+ 'build-backend'] != 'setuptools.build_meta':
126
+ new_config['build-system']['build-backend'] = 'setuptools.build_meta'
127
+ save_config(new_config, pyproject_path)
128
+
129
+ @staticmethod
130
+ def restore_pyproject_toml(project_dir: Path, original_toml_config: TOMLDocument):
131
+ pyproject_path = project_dir / "pyproject.toml"
132
+ if original_toml_config:
133
+ save_config(original_toml_config, pyproject_path)
134
+ logger.info("Restored original pyproject.toml")
135
+
136
+ @staticmethod
137
+ def create_setup_py_for_cython(config: DeployConfig, toml_config: TOMLDocument):
138
+ setup_py_path = config.project_dir / "setup.py"
139
+ if setup_py_path.exists():
140
+ # Check if the existing setup.py already uses cythonize
141
+ with open(setup_py_path, 'r', encoding='utf-8') as f:
142
+ setup_content = f.read()
143
+
144
+ if 'cythonize' in setup_content:
145
+ logger.info(f"Using existing setup.py with cythonize at {setup_py_path}")
146
+ return
147
+
148
+ # If cythonize is not present, raise an error
149
+ raise FileExistsError(
150
+ f"Cannot build Cython code: setup.py already exists at {setup_py_path}\n"
151
+ f"\n"
152
+ f"In Cython build mode, this tool generates its own setup.py file optimized for "
153
+ f"Cython compilation. An existing setup.py would be overwritten, potentially "
154
+ f"causing build errors or losing your custom configuration.\n"
155
+ f"\n"
156
+ f"To proceed with Cython build:\n"
157
+ f" 1. Migrate your setup.py settings to pyproject.toml:\n"
158
+ f" - Move dependencies to [project.dependencies]\n"
159
+ f" - Move metadata (name, version, description) to [project]\n"
160
+ f" - Move entry points to [project.scripts]\n"
161
+ f" - Move build configuration to [build-system] or [tool] sections\n"
162
+ f" 2. Back up your current setup.py: mv setup.py setup.py.backup\n"
163
+ f" 3. Then retry the Cython build\n"
164
+ f"\n"
165
+ f"Alternative (quick start):\n"
166
+ f" - Just backup setup.py now: mv setup.py setup.py.backup\n"
167
+ f" - Retry the build, then migrate settings later by comparing\n"
168
+ f" setup.py.backup with the generated pyproject.toml\n"
169
+ f"\n"
170
+ f"Note: Modern Python projects use pyproject.toml (PEP 518/621) for "
171
+ f"configuration instead of setup.py. This approach provides better "
172
+ f"tooling integration and is the recommended standard."
173
+ )
174
+
175
+ if "authors" in toml_config["project"]:
176
+ author_names = ", ".join(p["name"] for p in toml_config["project"]["authors"] if "name" in p)
177
+ author_emails = ", ".join(p["email"] for p in toml_config["project"]["authors"] if "email" in p)
178
+ else:
179
+ author_names = ""
180
+ author_emails = ""
181
+ if "scripts" in toml_config["project"]:
182
+ entry_points = [f"{k}={v}" for k, v in toml_config["project"]["scripts"].items()]
183
+ else:
184
+ entry_points = []
185
+ setup_py_content = textwrap.dedent(f'''
186
+ import glob
187
+ from Cython.Build import cythonize
188
+ from setuptools import setup, find_packages
189
+ from setuptools.dist import Distribution
190
+ from setuptools.command.build_py import build_py as _build_py
191
+
192
+ py_files = glob.glob("{config.package_entry}/**/*.py", recursive=True)
193
+ py_files = [f for f in py_files if not f.endswith("__init__.py")]
194
+
195
+ class build_py(_build_py):
196
+ def find_package_modules(self, package, package_dir):
197
+ modules = super().find_package_modules(package, package_dir)
198
+ if self.distribution.ext_modules:
199
+ # Get the list of compiled module names
200
+ compiled_modules = {{ext.name for ext in self.distribution.ext_modules}}
201
+ # Filter out the modules that are compiled
202
+ modules = [
203
+ (pkg, mod, file) for (pkg, mod, file) in modules
204
+ if f"{{pkg}}.{{mod}}" not in compiled_modules
205
+ ]
206
+ return modules
207
+
208
+ class BinaryDistribution(Distribution):
209
+ def has_ext_modules(self):
210
+ return True
211
+
212
+ setup(
213
+ name="{toml_config["project"]["name"]}",
214
+ version="{toml_config["project"]["version"]}",
215
+ {f"author='{author_names}'," if author_names else ""}
216
+ {f"author_email='{author_emails}'," if author_emails else ""}
217
+ {f"description='{toml_config['project']['description']}'," if toml_config["project"].get("description", "") else ""}
218
+ {f"python_requires='{toml_config['project']['requires-python']}'," if toml_config["project"].get("requires-python") else ""}
219
+ {f"install_requires={toml_config['project']['dependencies']}," if toml_config["project"].get("dependencies") and len(toml_config["project"]["dependencies"]) > 0 else ""}
220
+ entry_points={{
221
+ 'console_scripts': {entry_points}
222
+ }},
223
+ packages=find_packages(where="{config.package_entry}"),
224
+ package_dir={{"": "{config.package_entry}"}},
225
+ include_package_data=True,
226
+ ext_modules=cythonize(
227
+ py_files,
228
+ compiler_directives={{'language_level': "3"}},
229
+ ),
230
+ distclass=BinaryDistribution,
231
+ setup_requires=["cython>=3.1"],
232
+ cmdclass={{'build_py': build_py}},
233
+ zip_safe=False
234
+ )
235
+ ''').strip()
236
+
237
+ with open(config.project_dir / "setup.py", 'w', encoding='utf-8') as f:
238
+ f.write(setup_py_content)
pkg_deploy/deploy.py ADDED
@@ -0,0 +1,410 @@
1
+ import os
2
+ import sys
3
+ import glob
4
+ import shutil
5
+ import logging
6
+ import argparse
7
+ import subprocess
8
+ from pathlib import Path
9
+ from tomlkit import TOMLDocument
10
+
11
+ from .upload import Upload, NexusUpload
12
+ from .version_managment import VersionManager
13
+ from .build import DeployConfig, CythonBuildStrategy, StandardBuildStrategy
14
+ from .utils import get_pypirc_info, get_credentials, is_uv_venv, validate_version_arg, load_config
15
+
16
+
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format="%(asctime)s [%(levelname)-5.5s] [%(name)-30.30s] [%(lineno)-4.4s] [%(processName)-12.12s]: %(message)s"
20
+ )
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def parse_args(args):
25
+ parser = argparse.ArgumentParser(
26
+ description="Modern Python Package Deployment Tool",
27
+ formatter_class=argparse.RawDescriptionHelpFormatter,
28
+ epilog="""
29
+ Examples:
30
+ # Deploy to PyPI, patch version
31
+ python deploy.py --package-name my-package --version-type patch
32
+
33
+ # Deploy to private Nexus, using cython
34
+ python deploy.py --package-name my-package --version-type minor
35
+ --repository-url https://nexus.example.com/repository/pypi-internal/
36
+ --username admin
37
+ --password secret
38
+
39
+ # Dry run
40
+ python deploy.py --package-name my-package --version-type patch --dry-run
41
+ """)
42
+
43
+ parser.add_argument(
44
+ "--project-dir",
45
+ type=Path,
46
+ default=Path.cwd(),
47
+ help="Project directory (default: current directory)"
48
+ )
49
+
50
+ parser.add_argument(
51
+ "--package-dir",
52
+ type=Path,
53
+ default=None,
54
+ help="Package directory (default: current directory)"
55
+ )
56
+
57
+ parser.add_argument(
58
+ "--version-type", "-vt",
59
+ default="patch",
60
+ help="Version bump type (default: patch)",
61
+ choices=["major", "minor", "patch", "alpha", "beta", "rc"]
62
+ )
63
+
64
+ parser.add_argument(
65
+ "--new-version", "-v",
66
+ type=validate_version_arg,
67
+ help="New version number, if not specified, a new version will be resolved by version-type"
68
+ )
69
+
70
+ parser.add_argument(
71
+ "--cython", "-c",
72
+ action="store_true",
73
+ help="Use Cython for compilation"
74
+ )
75
+
76
+ parser.add_argument(
77
+ "--cibuildwheel",
78
+ action="store_true",
79
+ help="Use cibuildwheel to build cython code"
80
+ )
81
+
82
+ parser.add_argument(
83
+ "--repository-name", "-rn",
84
+ help="Repository name (.pypirc)"
85
+ )
86
+
87
+ parser.add_argument(
88
+ "--repository-url", "-ru",
89
+ help="Repository URL"
90
+ )
91
+
92
+ parser.add_argument(
93
+ "--username", "-u",
94
+ help="Username for authentication"
95
+ )
96
+
97
+ parser.add_argument(
98
+ "--password", "-p",
99
+ help="Password for authentication"
100
+ )
101
+
102
+ parser.add_argument(
103
+ "--skip-git-push",
104
+ action="store_true",
105
+ help="Don't push version changes and new tag to Git repository after build"
106
+ )
107
+
108
+ parser.add_argument(
109
+ "--skip-git-status-check",
110
+ action="store_true",
111
+ help="Skip git status check before deployment"
112
+ )
113
+
114
+ parser.add_argument(
115
+ "--dry-run",
116
+ action="store_true",
117
+ help="Perform a dry run without actual deployment"
118
+ )
119
+
120
+ parser.add_argument(
121
+ "--verbose", "-V",
122
+ action="store_true",
123
+ help="Enable verbose logging"
124
+ )
125
+
126
+ args = parser.parse_args(args)
127
+ if not args.repository_url and not args.repository_name:
128
+ parser.error("Either --repository-url or --repository-name must be provided.")
129
+ return args
130
+
131
+
132
+ class PackageDeploy:
133
+ def __init__(self):
134
+ args = sys.argv[1:]
135
+ self.args = parse_args(args)
136
+ if not (self.args.project_dir / "pyproject.toml").exists():
137
+ raise ValueError(f"pyproject.toml not found under project directory: {self.args.project_dir}")
138
+ else:
139
+ pyproject_path = self.args.project_dir / "pyproject.toml"
140
+
141
+ if self.args.verbose:
142
+ logging.getLogger().setLevel(logging.DEBUG)
143
+
144
+ self.check_require_package(self.args.cython)
145
+
146
+ url, username, password = self.get_twine_upload_info()
147
+ toml_config = load_config(pyproject_path)
148
+ package_dir = self.resolve_package_dir(toml_config)
149
+
150
+ self.version_manager = VersionManager(pyproject_path, toml_config)
151
+ self.config = DeployConfig(
152
+ package_name=toml_config["project"]["name"],
153
+ project_dir=self.args.project_dir,
154
+ package_dir=package_dir,
155
+ package_entry=package_dir.name,
156
+ pyproject_path=pyproject_path,
157
+ version_type=self.args.version_type,
158
+ new_version=self.args.new_version,
159
+ use_cython=self.args.cython,
160
+ use_cibuildwheel=self.args.cibuildwheel,
161
+ is_uv_venv=is_uv_venv(),
162
+ repository_name=self.args.repository_name,
163
+ repository_url=url,
164
+ username=username,
165
+ password=password,
166
+ dry_run=self.args.dry_run
167
+ )
168
+ self.setup_file_exist = (self.config.project_dir / "setup.py").exists()
169
+
170
+ def deploy(self):
171
+ logger.info("=== Deployment Configuration ===")
172
+ for field, value in vars(self.config).items():
173
+ if field == "password":
174
+ logger.info(f"{field}: ***MASKED***")
175
+ else:
176
+ logger.info(f"{field}: {value}")
177
+ logger.info("=================================")
178
+
179
+ if self.config.dry_run:
180
+ logger.info("DRY RUN: Starting deployment simulation")
181
+ else:
182
+ logger.info(f"Starting deployment")
183
+
184
+ if not self.args.skip_git_status_check:
185
+ self.check_git_status()
186
+
187
+ try:
188
+ new_version = self.version_manager.bump_version(
189
+ version_type=self.config.version_type,
190
+ new_version=self.config.new_version,
191
+ dry_run=self.config.dry_run
192
+ )
193
+ if self.config.dry_run:
194
+ logger.info(f"DRY RUN: Would bump version to: {new_version}")
195
+ else:
196
+ logger.info(f"New version: {new_version}")
197
+
198
+ if self.config.use_cython:
199
+ build_strategy = CythonBuildStrategy()
200
+ else:
201
+ build_strategy = StandardBuildStrategy()
202
+
203
+ uploaded = False
204
+ if build_strategy.build(self.config, self.version_manager.toml_config):
205
+ upload_strategy = self.get_upload_strategy(self.config)
206
+ dist_dir = self.config.project_dir / "dist"
207
+ uploaded = upload_strategy.upload(self.config, dist_dir)
208
+
209
+ self.cleanup_build_files()
210
+
211
+ if uploaded and not self.args.skip_git_push:
212
+ self.git_push(new_version=new_version, dry_run=self.config.dry_run)
213
+ else:
214
+ self.git_roll_back()
215
+ logger.info('Deploy completed')
216
+ except Exception as e:
217
+ logger.error(f"Deployment failed, rolling back: {e}", exc_info=True)
218
+ self.git_roll_back()
219
+ return False
220
+
221
+ def get_twine_upload_info(self):
222
+ pypirc_info = get_pypirc_info()
223
+ repos = pypirc_info["repositories"]
224
+ if self.args.repository_name == "pypi":
225
+ url = None
226
+ username = "__token__"
227
+ password = None
228
+ if "pypi" in repos:
229
+ password = repos["pypi"].get("password")
230
+ if not password:
231
+ _, password = get_credentials(username=username, is_pypi=True)
232
+ elif self.args.repository_name and self.args.repository_name in repos:
233
+ repository_info = repos[self.args.repository_name]
234
+ url = repository_info.get("repository")
235
+ username = repository_info.get("username")
236
+ password = repository_info.get("password")
237
+ if not url:
238
+ raise ValueError(
239
+ f"Repository '{self.args.repository_name}' must have a 'repository' url section in .pypirc"
240
+ f"Only 'pypi' can omit the repository URL.")
241
+ if not username or not password:
242
+ username, password = get_credentials(
243
+ username=username,
244
+ password=password,
245
+ url=url
246
+ )
247
+ elif self.args.repository_name and not self.args.repository_url:
248
+ raise ValueError(
249
+ f"Repository '{self.args.repository_name}' not found in .pypirc. "
250
+ f"Please provide --repository-url or add required info in .pypirc"
251
+ )
252
+ else:
253
+ url = self.args.repository_url
254
+ username, password = get_credentials(username=self.args.username,
255
+ password=self.args.password,
256
+ url=url)
257
+ return url, username, password
258
+
259
+ @staticmethod
260
+ def check_require_package(cython: bool):
261
+ required_packages = ["build", "twine", "toml", "tomlkit"]
262
+ if cython:
263
+ required_packages.append("Cython")
264
+
265
+ missing_packages = []
266
+ for package in required_packages:
267
+ try:
268
+ __import__(package)
269
+ except ImportError:
270
+ missing_packages.append(package)
271
+
272
+ if missing_packages:
273
+ logger.error(f"Missing required packages: {', '.join(missing_packages)}")
274
+ logger.error(f"Install them with: pip install {' '.join(missing_packages)}")
275
+ raise ValueError("Missing required packages")
276
+
277
+ def resolve_package_dir(self, toml_config: TOMLDocument) -> Path:
278
+ package_dir = self.args.project_dir / toml_config["project"]["name"].replace("-", "_")
279
+ if self.args.package_dir is not None:
280
+ package_dir = self.args.package_dir
281
+ else:
282
+ pkg_dir_candidates = []
283
+
284
+ # Safely extract 'packages.find.where'
285
+ where = (
286
+ toml_config
287
+ .get("tool", {})
288
+ .get("setuptools", {})
289
+ .get("packages", {})
290
+ .get("find", {})
291
+ .get("where")
292
+ )
293
+ if where and isinstance(where, list):
294
+ pkg_dir_candidates.append(where[0])
295
+ elif where is not None:
296
+ logger.warning("'tool.setuptools.packages.find.where' is not a list; skipping.")
297
+
298
+ # Safely extract first value from 'package-dir' dict
299
+ package_dir_map = (
300
+ toml_config
301
+ .get("tool", {})
302
+ .get("setuptools", {})
303
+ .get("package-dir")
304
+ )
305
+ if package_dir_map and isinstance(package_dir_map, dict) and package_dir_map:
306
+ first_value = next(iter(package_dir_map.values()))
307
+ if isinstance(first_value, str):
308
+ pkg_dir_candidates.append(first_value)
309
+ else:
310
+ logger.warning("'tool.setuptools.package-dir' values should be strings; skipping.")
311
+
312
+ if len(set(pkg_dir_candidates)) > 1:
313
+ logger.warning(f"Package directory from toml are not the same: {pkg_dir_candidates}, use the default directory: {package_dir}")
314
+ elif len(pkg_dir_candidates) == 0:
315
+ logger.warning(f"No entry point find, use the default directory: {package_dir}")
316
+ else:
317
+ package_dir = self.args.project_dir / pkg_dir_candidates[0]
318
+
319
+ if not package_dir.exists():
320
+ raise FileNotFoundError(f"Failed to resolve package directory, directory not found: {package_dir}")
321
+ return package_dir
322
+
323
+ def cleanup_build_files(self):
324
+ logger.info('Deleting build, dist and egg-info files after deployment')
325
+ shutil.rmtree('dist', ignore_errors=True)
326
+ shutil.rmtree('build', ignore_errors=True)
327
+ shutil.rmtree(f'{self.config.package_dir}/{self.config.package_name}.egg-info', ignore_errors=True)
328
+ egg_info_name = self.config.package_name.replace("-", "_")
329
+ shutil.rmtree(f'{self.config.package_dir}/{egg_info_name}.egg-info', ignore_errors=True)
330
+ directory = self.config.package_dir
331
+ logger.debug(f"The directory is: {directory}")
332
+ c_files = glob.glob(os.path.join(directory, '**', '*.c'), recursive=True)
333
+ if not self.setup_file_exist:
334
+ Path("setup.py").unlink(missing_ok=True)
335
+ if c_files:
336
+ logger.info(f"Cleaning up c files: {c_files}")
337
+ for file_path in c_files:
338
+ Path(file_path).unlink(missing_ok=True)
339
+
340
+ def check_git_status(self):
341
+ logger.info("Checking git status, --porcelain to make sure git repo is clean")
342
+ result = subprocess.run(
343
+ ["git", "status", "--porcelain"],
344
+ cwd=self.config.project_dir,
345
+ capture_output=True,
346
+ text=True
347
+ )
348
+ if result.returncode != 0:
349
+ raise IOError(f"Git command failed: {result.stderr.strip()}")
350
+ if result.stdout.strip():
351
+ raise IOError(f"Git repo is NOT clean: \n{result.stdout}")
352
+
353
+ @staticmethod
354
+ def git_push(new_version: str, dry_run: bool = False):
355
+ try:
356
+ if dry_run:
357
+ logger.info("DRY RUN: Would run: git add .")
358
+ logger.info(f"DRY RUN: Would run: git commit -m 'Bump version to {new_version}'")
359
+ tag_name = f"v{new_version}"
360
+ logger.info(f"DRY RUN: Would create Git tag: {tag_name}")
361
+ logger.info("DRY RUN: Would run: git push --follow-tags")
362
+ logger.info('DRY RUN: Git push simulation completed')
363
+ else:
364
+ subprocess.check_output(['git', 'add', '.'], stderr=subprocess.STDOUT)
365
+ subprocess.check_output(['git', 'commit', '-m', f'Bump version to {new_version}'], stderr=subprocess.STDOUT)
366
+ tag_name = f"v{new_version}"
367
+
368
+ # Check if the tag already exists
369
+ result = subprocess.run(['git', 'tag', '-l', tag_name], capture_output=True, text=True)
370
+ if result.stdout.strip():
371
+ logger.warning(f"Warning: Git tag {tag_name} already exists, skipping tag creation")
372
+ else:
373
+ subprocess.check_output(['git', 'tag', '-a', tag_name, '-m', f'Release {tag_name}'], stderr=subprocess.STDOUT)
374
+ logger.info(f"Created Git tag: {tag_name}")
375
+
376
+ subprocess.check_output(['git', 'push', '--follow-tags'], stderr=subprocess.STDOUT)
377
+ logger.info('Pushing to github')
378
+ except subprocess.CalledProcessError as ex:
379
+ logger.error(f"Git command failed: {ex.output.decode()}")
380
+ logger.warning('Failed to push bump version commit. Please push manually.')
381
+ raise
382
+ except Exception as ex:
383
+ logger.error(f"Unexpected error: {ex}")
384
+ logger.warning('Failed to push bump version commit. Please push manually.')
385
+ raise
386
+
387
+ @staticmethod
388
+ def git_roll_back():
389
+ try:
390
+ subprocess.check_output(['git', 'restore', '.'], stderr=subprocess.STDOUT)
391
+ subprocess.check_output(['git', 'restore', '--staged', '.'], stderr=subprocess.STDOUT)
392
+ subprocess.check_output(['git', 'clean', '-fd'], stderr=subprocess.STDOUT)
393
+ logger.info('Restored changes')
394
+ except subprocess.CalledProcessError as ex:
395
+ logger.error(f"Git command failed: {ex.output.decode()}")
396
+ except Exception as ex:
397
+ logger.error(f"Unexpected error: {ex}")
398
+ logger.warning('Failed to roll back changes. Please roll back manually.')
399
+
400
+ @staticmethod
401
+ def get_upload_strategy(config) -> Upload:
402
+ return NexusUpload()
403
+
404
+
405
+ def main():
406
+ PackageDeploy().deploy()
407
+
408
+
409
+ if __name__ == "__main__":
410
+ main()