salt-bundle 0.0.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.
- salt_bundle-0.0.1/PKG-INFO +6 -0
- salt_bundle-0.0.1/pyproject.toml +13 -0
- salt_bundle-0.0.1/salt_bundle/__init__.py +21 -0
- salt_bundle-0.0.1/salt_bundle/cli.py +419 -0
- salt_bundle-0.0.1/salt_bundle/config.py +143 -0
- salt_bundle-0.0.1/salt_bundle/github.py +156 -0
- salt_bundle-0.0.1/salt_bundle/lockfile.py +88 -0
- salt_bundle-0.0.1/salt_bundle/models/__init__.py +27 -0
- salt_bundle-0.0.1/salt_bundle/models/config_models.py +25 -0
- salt_bundle-0.0.1/salt_bundle/models/index_models.py +20 -0
- salt_bundle-0.0.1/salt_bundle/models/lock_models.py +16 -0
- salt_bundle-0.0.1/salt_bundle/models/package_models.py +40 -0
- salt_bundle-0.0.1/salt_bundle/package.py +175 -0
- salt_bundle-0.0.1/salt_bundle/release.py +489 -0
- salt_bundle-0.0.1/salt_bundle/repository.py +236 -0
- salt_bundle-0.0.1/salt_bundle/resolver.py +155 -0
- salt_bundle-0.0.1/salt_bundle/utils/__init__.py +15 -0
- salt_bundle-0.0.1/salt_bundle/utils/fs.py +102 -0
- salt_bundle-0.0.1/salt_bundle/utils/hashing.py +49 -0
- salt_bundle-0.0.1/salt_bundle/utils/yaml.py +44 -0
- salt_bundle-0.0.1/salt_bundle/vendor.py +106 -0
- salt_bundle-0.0.1/salt_bundle.egg-info/PKG-INFO +6 -0
- salt_bundle-0.0.1/salt_bundle.egg-info/SOURCES.txt +25 -0
- salt_bundle-0.0.1/salt_bundle.egg-info/dependency_links.txt +1 -0
- salt_bundle-0.0.1/salt_bundle.egg-info/entry_points.txt +2 -0
- salt_bundle-0.0.1/salt_bundle.egg-info/top_level.txt +1 -0
- salt_bundle-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "salt-bundle"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Salt package manager prototype"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
salt-bundle = "salt_bundle.cli:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["setuptools>=64", "wheel"]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""salt-bundle - Salt package manager."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.1"
|
|
4
|
+
|
|
5
|
+
from . import (
|
|
6
|
+
config,
|
|
7
|
+
lockfile,
|
|
8
|
+
package,
|
|
9
|
+
repository,
|
|
10
|
+
resolver,
|
|
11
|
+
vendor,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"config",
|
|
16
|
+
"lockfile",
|
|
17
|
+
"package",
|
|
18
|
+
"repository",
|
|
19
|
+
"resolver",
|
|
20
|
+
"vendor",
|
|
21
|
+
]
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""CLI interface for salt-bundle."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
# Handle both package import and direct execution
|
|
9
|
+
try:
|
|
10
|
+
from . import config, lockfile, package, release, repository, resolver, vendor
|
|
11
|
+
from .models.config_models import ProjectConfig, RepositoryConfig
|
|
12
|
+
from .models.package_models import PackageMeta
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Direct execution - add parent directory to path
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
from salt_bundle import config, lockfile, package, release, repository, resolver, vendor
|
|
17
|
+
from salt_bundle.models.config_models import ProjectConfig, RepositoryConfig
|
|
18
|
+
from salt_bundle.models.package_models import PackageMeta
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
@click.option('--debug', is_flag=True, help='Enable debug mode')
|
|
23
|
+
@click.option('--quiet', is_flag=True, help='Suppress output')
|
|
24
|
+
@click.option('--project-dir', '-C', type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
25
|
+
help='Project directory (default: current directory)')
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def cli(ctx, debug, quiet, project_dir):
|
|
28
|
+
"""Salt package manager."""
|
|
29
|
+
ctx.ensure_object(dict)
|
|
30
|
+
ctx.obj['DEBUG'] = debug
|
|
31
|
+
ctx.obj['QUIET'] = quiet
|
|
32
|
+
ctx.obj['PROJECT_DIR'] = Path(project_dir) if project_dir else Path.cwd()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@cli.command()
|
|
36
|
+
@click.option('--project', 'config_type', flag_value='project', help='Initialize project configuration')
|
|
37
|
+
@click.option('--formula', 'config_type', flag_value='formula', help='Initialize formula configuration')
|
|
38
|
+
@click.option('--force', is_flag=True, help='Overwrite existing configuration')
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def init(ctx, config_type, force):
|
|
41
|
+
"""Initialize salt-bundle configuration."""
|
|
42
|
+
if not config_type:
|
|
43
|
+
click.echo("Error: Specify --project or --formula", err=True)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
project_dir = ctx.obj['PROJECT_DIR']
|
|
47
|
+
config_file = project_dir / '.saltbundle.yaml'
|
|
48
|
+
|
|
49
|
+
if config_file.exists() and not force:
|
|
50
|
+
click.echo(f"Error: {config_file} already exists. Use --force to overwrite.", err=True)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
if config_type == 'project':
|
|
54
|
+
name = click.prompt("Project name", default="my-project")
|
|
55
|
+
version = click.prompt("Version", default="0.1.0")
|
|
56
|
+
|
|
57
|
+
project_config = ProjectConfig(
|
|
58
|
+
project=name,
|
|
59
|
+
version=version,
|
|
60
|
+
vendor_dir="vendor",
|
|
61
|
+
repositories=[],
|
|
62
|
+
dependencies={}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
config.save_project_config(project_config, project_dir)
|
|
66
|
+
click.echo(f"Created project configuration: {config_file}")
|
|
67
|
+
|
|
68
|
+
elif config_type == 'formula':
|
|
69
|
+
name = click.prompt("Formula name")
|
|
70
|
+
version = click.prompt("Version", default="1.0.0")
|
|
71
|
+
description = click.prompt("Description", default="")
|
|
72
|
+
|
|
73
|
+
# Salt compatibility
|
|
74
|
+
salt_min = click.prompt("Salt min version", default="", show_default=False)
|
|
75
|
+
salt_max = click.prompt("Salt max version", default="", show_default=False)
|
|
76
|
+
|
|
77
|
+
# Import SaltCompatibility model
|
|
78
|
+
from salt_bundle.models.package_models import SaltCompatibility
|
|
79
|
+
|
|
80
|
+
salt_compat = None
|
|
81
|
+
if salt_min or salt_max:
|
|
82
|
+
salt_compat = SaltCompatibility(
|
|
83
|
+
min_version=salt_min if salt_min else None,
|
|
84
|
+
max_version=salt_max if salt_max else None
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
formula_meta = PackageMeta(
|
|
88
|
+
name=name,
|
|
89
|
+
version=version,
|
|
90
|
+
description=description if description else None,
|
|
91
|
+
salt=salt_compat
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
config.save_package_meta(formula_meta, project_dir)
|
|
95
|
+
click.echo(f"Created formula configuration: {config_file}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command()
|
|
99
|
+
@click.option('--output-dir', '-o', type=click.Path(), help='Output directory')
|
|
100
|
+
@click.pass_context
|
|
101
|
+
def pack(ctx, output_dir):
|
|
102
|
+
"""Pack formula into tar.gz archive."""
|
|
103
|
+
try:
|
|
104
|
+
project_dir = ctx.obj['PROJECT_DIR']
|
|
105
|
+
output_path = Path(output_dir) if output_dir else project_dir
|
|
106
|
+
archive_path = package.pack_formula(project_dir, output_path)
|
|
107
|
+
click.echo(f"Created package: {archive_path}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
click.echo(f"Error: {e}", err=True)
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@cli.command()
|
|
114
|
+
@click.argument('directory', type=click.Path(exists=True), default='.')
|
|
115
|
+
@click.option('--output-dir', '-o', type=click.Path(), help='Output directory for index.yaml (default: same as input directory)')
|
|
116
|
+
@click.option('--base-url', '-u', help='Base URL for package links in index (e.g., https://example.com/repo/)')
|
|
117
|
+
@click.pass_context
|
|
118
|
+
def index(ctx, directory, output_dir, base_url):
|
|
119
|
+
"""Generate or update repository index."""
|
|
120
|
+
try:
|
|
121
|
+
repo_dir = Path(directory)
|
|
122
|
+
output_path = Path(output_dir) if output_dir else repo_dir
|
|
123
|
+
|
|
124
|
+
# Ensure output directory exists
|
|
125
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
idx = repository.generate_index(repo_dir, base_url=base_url)
|
|
128
|
+
repository.save_index(idx, output_path)
|
|
129
|
+
|
|
130
|
+
click.echo(f"Generated index with {len(idx.packages)} packages")
|
|
131
|
+
for name, entries in idx.packages.items():
|
|
132
|
+
click.echo(f" {name}: {len(entries)} versions")
|
|
133
|
+
|
|
134
|
+
if output_path != repo_dir:
|
|
135
|
+
click.echo(f"Index saved to: {output_path / 'index.yaml'}")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
click.echo(f"Error: {e}", err=True)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@cli.command('add-repo')
|
|
142
|
+
@click.option('--name', required=True, help='Repository name')
|
|
143
|
+
@click.option('--url', required=True, help='Repository URL')
|
|
144
|
+
@click.pass_context
|
|
145
|
+
def add_repo(ctx, name, url):
|
|
146
|
+
"""Add repository to user configuration."""
|
|
147
|
+
try:
|
|
148
|
+
config.add_user_repository(name, url)
|
|
149
|
+
click.echo(f"Added repository: {name} -> {url}")
|
|
150
|
+
except ValueError as e:
|
|
151
|
+
click.echo(f"Error: {e}", err=True)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@cli.command()
|
|
156
|
+
@click.option('--no-lock', is_flag=True, help='Ignore lock file')
|
|
157
|
+
@click.option('--update-lock', is_flag=True, help='Update lock file')
|
|
158
|
+
@click.pass_context
|
|
159
|
+
def install(ctx, no_lock, update_lock):
|
|
160
|
+
"""Install project dependencies."""
|
|
161
|
+
try:
|
|
162
|
+
project_dir = ctx.obj['PROJECT_DIR']
|
|
163
|
+
|
|
164
|
+
# Load project config
|
|
165
|
+
try:
|
|
166
|
+
proj_config = config.load_project_config(project_dir)
|
|
167
|
+
except FileNotFoundError:
|
|
168
|
+
click.echo("Error: .saltbundle.yaml not found. Run 'salt-bundle init --project' first.", err=True)
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
|
|
171
|
+
vendor_dir = vendor.get_vendor_dir(project_dir, proj_config.vendor_dir)
|
|
172
|
+
vendor.ensure_vendor_dir(vendor_dir)
|
|
173
|
+
|
|
174
|
+
# Get all repositories (project + user)
|
|
175
|
+
user_config = config.load_user_config()
|
|
176
|
+
all_repos = proj_config.repositories + user_config.repositories
|
|
177
|
+
|
|
178
|
+
if not all_repos:
|
|
179
|
+
click.echo("Warning: No repositories configured", err=True)
|
|
180
|
+
|
|
181
|
+
# Check if we need to resolve dependencies
|
|
182
|
+
lock_file_exists = lockfile.lockfile_exists(project_dir)
|
|
183
|
+
|
|
184
|
+
if lock_file_exists and not no_lock and not update_lock:
|
|
185
|
+
# Install from lock file
|
|
186
|
+
click.echo("Installing from lock file...")
|
|
187
|
+
lock = lockfile.load_lockfile(project_dir)
|
|
188
|
+
else:
|
|
189
|
+
# Resolve dependencies
|
|
190
|
+
click.echo("Resolving dependencies...")
|
|
191
|
+
lock = lockfile.LockFile()
|
|
192
|
+
|
|
193
|
+
for dep_name, dep_constraint in proj_config.dependencies.items():
|
|
194
|
+
resolved = None
|
|
195
|
+
|
|
196
|
+
# Try each repository
|
|
197
|
+
for repo in all_repos:
|
|
198
|
+
try:
|
|
199
|
+
idx = repository.fetch_index(repo.url)
|
|
200
|
+
if dep_name in idx.packages:
|
|
201
|
+
resolved_entry = resolver.resolve_version(dep_constraint, idx.packages[dep_name])
|
|
202
|
+
if resolved_entry:
|
|
203
|
+
lockfile.add_locked_dependency(
|
|
204
|
+
lock,
|
|
205
|
+
dep_name,
|
|
206
|
+
resolved_entry.version,
|
|
207
|
+
repo.name,
|
|
208
|
+
resolved_entry.url,
|
|
209
|
+
resolved_entry.digest
|
|
210
|
+
)
|
|
211
|
+
resolved = resolved_entry
|
|
212
|
+
break
|
|
213
|
+
except Exception as e:
|
|
214
|
+
click.echo(f"Warning: Failed to fetch from {repo.name}: {e}", err=True)
|
|
215
|
+
|
|
216
|
+
if not resolved:
|
|
217
|
+
click.echo(f"Error: Could not resolve dependency: {dep_name} {dep_constraint}", err=True)
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
|
|
220
|
+
# Save lock file
|
|
221
|
+
lockfile.save_lockfile(lock, project_dir)
|
|
222
|
+
|
|
223
|
+
# Install packages
|
|
224
|
+
for dep_name, locked_dep in lock.dependencies.items():
|
|
225
|
+
click.echo(f"Installing {dep_name} {locked_dep.version}...")
|
|
226
|
+
|
|
227
|
+
# Find repository URL
|
|
228
|
+
repo_url = None
|
|
229
|
+
for repo in all_repos:
|
|
230
|
+
if repo.name == locked_dep.repository:
|
|
231
|
+
repo_url = repo.url
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
if not repo_url:
|
|
235
|
+
click.echo(f"Error: Repository not found: {locked_dep.repository}", err=True)
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
|
|
238
|
+
# Download package
|
|
239
|
+
archive_path = repository.download_package(
|
|
240
|
+
locked_dep.url,
|
|
241
|
+
repo_url,
|
|
242
|
+
locked_dep.digest
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Install to vendor
|
|
246
|
+
vendor.install_package_to_vendor(archive_path, dep_name, vendor_dir)
|
|
247
|
+
|
|
248
|
+
click.echo("Installation complete!")
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
click.echo(f"Error: {e}", err=True)
|
|
252
|
+
if ctx.obj.get('DEBUG'):
|
|
253
|
+
import traceback
|
|
254
|
+
traceback.print_exc()
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@cli.command()
|
|
259
|
+
@click.pass_context
|
|
260
|
+
def vendor_cmd(ctx):
|
|
261
|
+
"""Install dependencies from lock file (reproducible deploy)."""
|
|
262
|
+
try:
|
|
263
|
+
# Reuse install logic with --no-lock flag behavior
|
|
264
|
+
ctx.invoke(install, no_lock=False, update_lock=False)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
click.echo(f"Error: {e}", err=True)
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@cli.command()
|
|
271
|
+
@click.pass_context
|
|
272
|
+
def verify(ctx):
|
|
273
|
+
"""Verify project dependencies integrity."""
|
|
274
|
+
try:
|
|
275
|
+
project_dir = ctx.obj['PROJECT_DIR']
|
|
276
|
+
|
|
277
|
+
# Load lock file
|
|
278
|
+
try:
|
|
279
|
+
lock = lockfile.load_lockfile(project_dir)
|
|
280
|
+
except FileNotFoundError:
|
|
281
|
+
click.echo("Error: salt-bundle.lock not found", err=True)
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
# Load project config
|
|
285
|
+
proj_config = config.load_project_config(project_dir)
|
|
286
|
+
vendor_dir = vendor.get_vendor_dir(project_dir, proj_config.vendor_dir)
|
|
287
|
+
|
|
288
|
+
errors = []
|
|
289
|
+
|
|
290
|
+
for dep_name, locked_dep in lock.dependencies.items():
|
|
291
|
+
# Check if installed
|
|
292
|
+
if not vendor.is_package_installed(dep_name, vendor_dir):
|
|
293
|
+
errors.append(f" {dep_name}: not installed")
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Check .saltbundle.yaml exists
|
|
297
|
+
package_meta_file = vendor_dir / dep_name / '.saltbundle.yaml'
|
|
298
|
+
if not package_meta_file.exists():
|
|
299
|
+
errors.append(f" {dep_name}: .saltbundle.yaml missing")
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
click.echo(f"✓ {dep_name} {locked_dep.version}")
|
|
303
|
+
|
|
304
|
+
if errors:
|
|
305
|
+
click.echo("\nErrors found:")
|
|
306
|
+
for error in errors:
|
|
307
|
+
click.echo(error, err=True)
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
else:
|
|
310
|
+
click.echo("\nAll dependencies verified successfully!")
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
click.echo(f"Error: {e}", err=True)
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@cli.command()
|
|
318
|
+
@click.option('--formulas-dir', '-f', type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
319
|
+
default='.', help='Directory containing formulas (default: current directory)')
|
|
320
|
+
@click.option('--repo-dir', '-r', type=click.Path(file_okay=False, dir_okay=True), required=True,
|
|
321
|
+
help='Repository directory where packages will be published')
|
|
322
|
+
@click.option('--skip-packaging', is_flag=True, help='Skip packaging step (use existing .tgz files)')
|
|
323
|
+
@click.option('--create-tags', is_flag=True, help='Create local git tags for releases')
|
|
324
|
+
@click.option('--github-release', is_flag=True, help='Create GitHub releases and upload packages (requires GITHUB_TOKEN and GITHUB_REPOSITORY env vars)')
|
|
325
|
+
@click.option('--index-branch', type=str, help='Git branch to commit index.yaml (e.g., gh-pages)')
|
|
326
|
+
@click.option('--dry-run', is_flag=True, help='Show what would be done without doing it')
|
|
327
|
+
@click.option('--single', is_flag=True, help='Treat formulas-dir as a single formula directory (not subdirectories)')
|
|
328
|
+
@click.pass_context
|
|
329
|
+
def release_cmd(ctx, formulas_dir, repo_dir, skip_packaging, create_tags, github_release, index_branch, dry_run, single):
|
|
330
|
+
"""Release formulas to repository.
|
|
331
|
+
|
|
332
|
+
This command automates the process of:
|
|
333
|
+
1. Discovering formulas in the specified directory
|
|
334
|
+
2. Detecting new versions (not in repository)
|
|
335
|
+
3. Packaging formulas (unless --skip-packaging)
|
|
336
|
+
4. Publishing to repository
|
|
337
|
+
5. Updating repository index
|
|
338
|
+
6. Optionally creating git tags
|
|
339
|
+
7. Optionally creating GitHub releases with package uploads
|
|
340
|
+
8. Optionally committing index.yaml to a separate branch
|
|
341
|
+
|
|
342
|
+
By default, searches for formulas in subdirectories. Use --single to treat
|
|
343
|
+
the directory itself as a single formula.
|
|
344
|
+
|
|
345
|
+
GitHub Integration:
|
|
346
|
+
When --github-release is used, requires:
|
|
347
|
+
- GITHUB_TOKEN: Personal access token with repo permissions
|
|
348
|
+
- GITHUB_REPOSITORY: Repository in format 'owner/repo'
|
|
349
|
+
|
|
350
|
+
Creates GitHub releases with tag format: {package-name}-{version}
|
|
351
|
+
Uploads .tgz packages as release assets.
|
|
352
|
+
|
|
353
|
+
Index Branch (like Helm Chart Releaser):
|
|
354
|
+
Use --index-branch to commit index.yaml to a separate orphan branch:
|
|
355
|
+
--index-branch gh-pages
|
|
356
|
+
This allows serving the index via GitHub Pages or keeping it separate from source.
|
|
357
|
+
"""
|
|
358
|
+
try:
|
|
359
|
+
formulas_path = Path(formulas_dir)
|
|
360
|
+
repo_path = Path(repo_dir)
|
|
361
|
+
|
|
362
|
+
if dry_run:
|
|
363
|
+
click.echo("=== DRY RUN MODE ===")
|
|
364
|
+
|
|
365
|
+
mode = "single formula" if single else "multiple formulas"
|
|
366
|
+
click.echo(f"Mode: {mode}")
|
|
367
|
+
click.echo(f"Formulas directory: {formulas_path}")
|
|
368
|
+
click.echo(f"Repository directory: {repo_path}")
|
|
369
|
+
click.echo()
|
|
370
|
+
|
|
371
|
+
# Run release process
|
|
372
|
+
released, errors = release.release_formulas(
|
|
373
|
+
formulas_path,
|
|
374
|
+
repo_path,
|
|
375
|
+
skip_packaging=skip_packaging,
|
|
376
|
+
create_tags=create_tags,
|
|
377
|
+
dry_run=dry_run,
|
|
378
|
+
single_formula=single,
|
|
379
|
+
github_release=github_release,
|
|
380
|
+
index_branch=index_branch
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Summary
|
|
384
|
+
click.echo()
|
|
385
|
+
click.echo("=" * 50)
|
|
386
|
+
click.echo("RELEASE SUMMARY")
|
|
387
|
+
click.echo("=" * 50)
|
|
388
|
+
|
|
389
|
+
if released:
|
|
390
|
+
click.echo(f"\n✓ Released {len(released)} package(s):")
|
|
391
|
+
for formula in released:
|
|
392
|
+
click.echo(f" - {formula.name} {formula.version}")
|
|
393
|
+
else:
|
|
394
|
+
click.echo("\nNo packages released")
|
|
395
|
+
|
|
396
|
+
if errors:
|
|
397
|
+
click.echo(f"\n✗ Errors ({len(errors)}):")
|
|
398
|
+
for error in errors:
|
|
399
|
+
click.echo(f" - {error}", err=True)
|
|
400
|
+
sys.exit(1)
|
|
401
|
+
|
|
402
|
+
if dry_run:
|
|
403
|
+
click.echo("\n[DRY RUN] No changes were made")
|
|
404
|
+
|
|
405
|
+
except Exception as e:
|
|
406
|
+
click.echo(f"Error: {e}", err=True)
|
|
407
|
+
if ctx.obj.get('DEBUG'):
|
|
408
|
+
import traceback
|
|
409
|
+
traceback.print_exc()
|
|
410
|
+
sys.exit(1)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def main():
|
|
414
|
+
"""Entry point for CLI."""
|
|
415
|
+
cli(obj={})
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == '__main__':
|
|
419
|
+
main()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Configuration management for salt-bundle."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .models.config_models import ProjectConfig, UserConfig, RepositoryConfig
|
|
8
|
+
from .models.package_models import PackageMeta
|
|
9
|
+
from .utils.yaml import load_yaml, dump_yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_config_dir() -> Path:
|
|
13
|
+
"""Get user configuration directory (XDG compliant).
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Path to config directory (~/.config/salt-bundle)
|
|
17
|
+
"""
|
|
18
|
+
xdg_config = os.environ.get('XDG_CONFIG_HOME')
|
|
19
|
+
if xdg_config:
|
|
20
|
+
config_dir = Path(xdg_config) / 'salt-bundle'
|
|
21
|
+
else:
|
|
22
|
+
config_dir = Path.home() / '.config' / 'salt-bundle'
|
|
23
|
+
|
|
24
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return config_dir
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_cache_dir() -> Path:
|
|
29
|
+
"""Get user cache directory (XDG compliant).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Path to cache directory (~/.cache/salt-bundle)
|
|
33
|
+
"""
|
|
34
|
+
xdg_cache = os.environ.get('XDG_CACHE_HOME')
|
|
35
|
+
if xdg_cache:
|
|
36
|
+
cache_dir = Path(xdg_cache) / 'salt-bundle'
|
|
37
|
+
else:
|
|
38
|
+
cache_dir = Path.home() / '.cache' / 'salt-bundle'
|
|
39
|
+
|
|
40
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
return cache_dir
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_user_config() -> UserConfig:
|
|
45
|
+
"""Load user global configuration.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
UserConfig object (returns empty config if file doesn't exist)
|
|
49
|
+
"""
|
|
50
|
+
config_file = get_config_dir() / 'config.yaml'
|
|
51
|
+
|
|
52
|
+
if not config_file.exists():
|
|
53
|
+
return UserConfig()
|
|
54
|
+
|
|
55
|
+
data = load_yaml(config_file)
|
|
56
|
+
return UserConfig(**data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_user_config(config: UserConfig) -> None:
|
|
60
|
+
"""Save user global configuration.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
config: UserConfig object to save
|
|
64
|
+
"""
|
|
65
|
+
config_file = get_config_dir() / 'config.yaml'
|
|
66
|
+
dump_yaml(config.model_dump(), config_file)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_user_repository(name: str, url: str) -> None:
|
|
70
|
+
"""Add repository to user configuration.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
name: Repository name
|
|
74
|
+
url: Repository URL
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If repository with same name already exists
|
|
78
|
+
"""
|
|
79
|
+
config = load_user_config()
|
|
80
|
+
|
|
81
|
+
# Check if repository with same name exists
|
|
82
|
+
for repo in config.repositories:
|
|
83
|
+
if repo.name == name:
|
|
84
|
+
raise ValueError(f"Repository '{name}' already exists")
|
|
85
|
+
|
|
86
|
+
config.repositories.append(RepositoryConfig(name=name, url=url))
|
|
87
|
+
save_user_config(config)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_project_config(project_dir: Path | str = Path.cwd()) -> ProjectConfig:
|
|
91
|
+
"""Load project configuration from .saltbundle.yaml.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
project_dir: Project directory (defaults to current directory)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
ProjectConfig object
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
FileNotFoundError: If .saltbundle.yaml doesn't exist
|
|
101
|
+
"""
|
|
102
|
+
config_file = Path(project_dir) / '.saltbundle.yaml'
|
|
103
|
+
data = load_yaml(config_file)
|
|
104
|
+
return ProjectConfig(**data)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def save_project_config(config: ProjectConfig, project_dir: Path | str = Path.cwd()) -> None:
|
|
108
|
+
"""Save project configuration to .saltbundle.yaml.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
config: ProjectConfig object to save
|
|
112
|
+
project_dir: Project directory (defaults to current directory)
|
|
113
|
+
"""
|
|
114
|
+
config_file = Path(project_dir) / '.saltbundle.yaml'
|
|
115
|
+
dump_yaml(config.model_dump(exclude_none=True), config_file)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_package_meta(package_dir: Path | str = Path.cwd()) -> PackageMeta:
|
|
119
|
+
"""Load package metadata from .saltbundle.yaml.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
package_dir: Package directory (defaults to current directory)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
PackageMeta object
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
FileNotFoundError: If .saltbundle.yaml doesn't exist
|
|
129
|
+
"""
|
|
130
|
+
meta_file = Path(package_dir) / '.saltbundle.yaml'
|
|
131
|
+
data = load_yaml(meta_file)
|
|
132
|
+
return PackageMeta(**data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def save_package_meta(meta: PackageMeta, package_dir: Path | str = Path.cwd()) -> None:
|
|
136
|
+
"""Save package metadata to .saltbundle.yaml.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
meta: PackageMeta object to save
|
|
140
|
+
package_dir: Package directory (defaults to current directory)
|
|
141
|
+
"""
|
|
142
|
+
meta_file = Path(package_dir) / '.saltbundle.yaml'
|
|
143
|
+
dump_yaml(meta.model_dump(exclude_none=True), meta_file)
|