splent-cli 1.28.2__tar.gz → 1.29.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.
- {splent_cli-1.28.2/src/splent_cli.egg-info → splent_cli-1.29.1}/PKG-INFO +1 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/pyproject.toml +1 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_release.py +96 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_build.py +42 -5
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_create.py +12 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_deploy.py +45 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_env.py +9 -2
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_release.py +123 -58
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_sync_template.py +11 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/release/release_resume.py +16 -9
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/tokens_setup.py +4 -1
- splent_cli-1.29.1/src/splent_cli/services/image_registry.py +173 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/marketplace.py +69 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/registry.py +100 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/release.py +64 -10
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/release_gate.py +107 -18
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/git_url.py +21 -3
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/template_drift.py +25 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/toml_edit.py +48 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/LICENSE +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/README.md +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/setup.cfg +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.29.1
|
|
4
4
|
Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
|
|
5
5
|
Author-email: DiversoLab <diversolab@us.es>
|
|
6
6
|
Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
|
|
@@ -3,6 +3,7 @@ feature:release — Release a feature: bump version, tag, publish to the git for
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
+
import json
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
9
|
import shutil
|
|
@@ -363,6 +364,83 @@ def _extract_commands(commands_path: Path) -> list[str]:
|
|
|
363
364
|
return sorted(set(re.findall(r"""@click\.command\s*\(\s*['"]([^'"]+)['"]""", text)))
|
|
364
365
|
|
|
365
366
|
|
|
367
|
+
def _call_name(node) -> str:
|
|
368
|
+
"""``click.command`` for the decorator call ``@click.command(...)``."""
|
|
369
|
+
func = node.func if isinstance(node, ast.Call) else node
|
|
370
|
+
parts = []
|
|
371
|
+
while isinstance(func, ast.Attribute):
|
|
372
|
+
parts.append(func.attr)
|
|
373
|
+
func = func.value
|
|
374
|
+
if isinstance(func, ast.Name):
|
|
375
|
+
parts.append(func.id)
|
|
376
|
+
return ".".join(reversed(parts))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _literal(node):
|
|
380
|
+
try:
|
|
381
|
+
return ast.literal_eval(node)
|
|
382
|
+
except (ValueError, SyntaxError, TypeError):
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _first_line(text) -> str:
|
|
387
|
+
for line in (text or "").strip().splitlines():
|
|
388
|
+
if line.strip():
|
|
389
|
+
return line.strip()
|
|
390
|
+
return ""
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _extract_command_help(commands_path: Path) -> dict:
|
|
394
|
+
"""What each command a feature adds is for, and how it is called.
|
|
395
|
+
|
|
396
|
+
``{name: {"usage": "EMAIL ROLE [OPTIONS]", "help": "..."}}``, read from
|
|
397
|
+
the source without importing it: the arguments in the order they are
|
|
398
|
+
declared, in click's uppercase, ``[OPTIONS]`` when there are any, and the
|
|
399
|
+
first line of ``short_help``, ``help`` or the docstring. The marketplace
|
|
400
|
+
shows it next to each command, which used to be a bare name.
|
|
401
|
+
"""
|
|
402
|
+
if not commands_path.exists():
|
|
403
|
+
return {}
|
|
404
|
+
try:
|
|
405
|
+
tree = ast.parse(commands_path.read_text())
|
|
406
|
+
except SyntaxError:
|
|
407
|
+
return {}
|
|
408
|
+
found = {}
|
|
409
|
+
for node in ast.walk(tree):
|
|
410
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
411
|
+
continue
|
|
412
|
+
name, help_text, usage, has_options = None, "", [], False
|
|
413
|
+
for deco in node.decorator_list:
|
|
414
|
+
if not isinstance(deco, ast.Call):
|
|
415
|
+
continue
|
|
416
|
+
kind = _call_name(deco)
|
|
417
|
+
if kind.endswith("command") and deco.args:
|
|
418
|
+
name = _literal(deco.args[0])
|
|
419
|
+
kwargs = {k.arg: _literal(k.value) for k in deco.keywords}
|
|
420
|
+
help_text = kwargs.get("short_help") or kwargs.get("help") or ""
|
|
421
|
+
elif kind.endswith("argument") and deco.args:
|
|
422
|
+
arg = _literal(deco.args[0])
|
|
423
|
+
if not isinstance(arg, str):
|
|
424
|
+
continue
|
|
425
|
+
kwargs = {k.arg: _literal(k.value) for k in deco.keywords}
|
|
426
|
+
metavar = arg.upper().replace("-", "_")
|
|
427
|
+
if kwargs.get("nargs") == -1:
|
|
428
|
+
metavar += "..."
|
|
429
|
+
optional = kwargs.get("required") is False or "default" in kwargs
|
|
430
|
+
usage.append(f"[{metavar}]" if optional else metavar)
|
|
431
|
+
elif kind.endswith("option"):
|
|
432
|
+
has_options = True
|
|
433
|
+
if not isinstance(name, str):
|
|
434
|
+
continue
|
|
435
|
+
if has_options:
|
|
436
|
+
usage.append("[OPTIONS]")
|
|
437
|
+
found[name] = {
|
|
438
|
+
"usage": " ".join(usage),
|
|
439
|
+
"help": _first_line(help_text) or _first_line(ast.get_docstring(node)),
|
|
440
|
+
}
|
|
441
|
+
return dict(sorted(found.items()))
|
|
442
|
+
|
|
443
|
+
|
|
366
444
|
def _feature_sources(src_dir: Path):
|
|
367
445
|
"""The feature's own Python files, without its tests.
|
|
368
446
|
|
|
@@ -693,6 +771,7 @@ def infer_contract(
|
|
|
693
771
|
templates = _extract_templates(src_dir)
|
|
694
772
|
template_hook_slots = _extract_template_hook_slots(src_dir)
|
|
695
773
|
commands = _extract_commands(src_dir / "commands.py")
|
|
774
|
+
commands_help = _extract_command_help(src_dir / "commands.py")
|
|
696
775
|
docker = _extract_docker(feature_root)
|
|
697
776
|
docker_contract = _extract_docker_contract(feature_root)
|
|
698
777
|
req_features, env_vars = _scan_dependencies(src_dir, feature_name)
|
|
@@ -751,6 +830,7 @@ def infer_contract(
|
|
|
751
830
|
"blueprints": blueprints,
|
|
752
831
|
"models": models,
|
|
753
832
|
"commands": commands,
|
|
833
|
+
"commands_help": commands_help,
|
|
754
834
|
"hooks": hooks,
|
|
755
835
|
"services": services,
|
|
756
836
|
"signals": signals_provided,
|
|
@@ -945,7 +1025,8 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
|
|
|
945
1025
|
f"signals = {_toml_value(contract.get('signals', []))}\n"
|
|
946
1026
|
f"translations = {_toml_value(contract.get('translations', []))}\n"
|
|
947
1027
|
f"docker = {_toml_value(contract['docker'])}\n"
|
|
948
|
-
"
|
|
1028
|
+
+ _commands_help_block(contract.get("commands_help") or {})
|
|
1029
|
+
+ "\n"
|
|
949
1030
|
"[tool.splent.contract.requires]\n"
|
|
950
1031
|
f"features = {_toml_value(contract['requires_features'])}\n"
|
|
951
1032
|
+ (
|
|
@@ -989,6 +1070,20 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
|
|
|
989
1070
|
path.write_text(text + contract_block + refinement_block)
|
|
990
1071
|
|
|
991
1072
|
|
|
1073
|
+
def _commands_help_block(commands_help: dict) -> str:
|
|
1074
|
+
"""The ``[tool.splent.contract.provides.commands_help]`` table, or nothing."""
|
|
1075
|
+
if not commands_help:
|
|
1076
|
+
return ""
|
|
1077
|
+
lines = ["\n[tool.splent.contract.provides.commands_help]\n"]
|
|
1078
|
+
for name, entry in commands_help.items():
|
|
1079
|
+
key = name if re.fullmatch(r"[A-Za-z0-9_-]+", name) else json.dumps(name)
|
|
1080
|
+
lines.append(
|
|
1081
|
+
f"{key} = {{ usage = {json.dumps(entry.get('usage', ''))}, "
|
|
1082
|
+
f"help = {json.dumps(entry.get('help', ''))} }}\n"
|
|
1083
|
+
)
|
|
1084
|
+
return "".join(lines)
|
|
1085
|
+
|
|
1086
|
+
|
|
992
1087
|
# ── Compile assets ────────────────────────────────────────────────────
|
|
993
1088
|
|
|
994
1089
|
|
|
@@ -88,6 +88,16 @@ OPTIONAL_DEPLOY_SETTINGS: tuple[tuple[str, str, tuple[str, ...]], ...] = (
|
|
|
88
88
|
"trusted proxy. true forces it on every cookie, false never sets it.",
|
|
89
89
|
),
|
|
90
90
|
),
|
|
91
|
+
(
|
|
92
|
+
"SPLENT_IMAGE",
|
|
93
|
+
"git.splent.io/<org>/<product>:stable",
|
|
94
|
+
(
|
|
95
|
+
"Image the web service runs. Unset means the image built on this",
|
|
96
|
+
"host. Set it to the image a release publishes, tagged stable, to",
|
|
97
|
+
"run releases without building here. Every deploy pulls it, and the",
|
|
98
|
+
"autodeploy feature follows it.",
|
|
99
|
+
),
|
|
100
|
+
),
|
|
91
101
|
(
|
|
92
102
|
"MAX_CONTENT_LENGTH",
|
|
93
103
|
"64M",
|
|
@@ -551,9 +561,34 @@ def _apply_restart_policy(svc_def: dict) -> None:
|
|
|
551
561
|
svc_def["restart"] = RESTART_POLICY
|
|
552
562
|
|
|
553
563
|
|
|
564
|
+
def resolve_product_placeholders(value: str, product: str, *, prod: bool) -> str:
|
|
565
|
+
"""Resolve ``__PRODUCT__`` in a feature's value for this product.
|
|
566
|
+
|
|
567
|
+
``__PRODUCT___web`` is the product's web container, which is
|
|
568
|
+
``<product>_web`` in development and ``<product>_web_deploy`` in
|
|
569
|
+
production (the prod compose template names it). Every other
|
|
570
|
+
``__PRODUCT__`` is the product name. product:build and
|
|
571
|
+
product:env --merge --prod both write .env.deploy, so both go through
|
|
572
|
+
this: the merge used to replace the plain name only and point a feature
|
|
573
|
+
at the development container's name in production.
|
|
574
|
+
"""
|
|
575
|
+
if "__PRODUCT__" not in value:
|
|
576
|
+
return value
|
|
577
|
+
if prod:
|
|
578
|
+
value = value.replace("__PRODUCT___web", f"{product}_web_deploy")
|
|
579
|
+
return value.replace("__PRODUCT__", product)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def local_or_registry_image(product: str) -> str:
|
|
583
|
+
"""The web image of a deploy: ``SPLENT_IMAGE`` when set, else the local build."""
|
|
584
|
+
return "${SPLENT_IMAGE:-" + product + ":latest}"
|
|
585
|
+
|
|
586
|
+
|
|
554
587
|
def apply_production_policies(compose_data: dict, product: str) -> dict:
|
|
555
588
|
"""Give every deployed service a restart policy, and the web and database
|
|
556
|
-
services a healthcheck, unless the compose file already set one.
|
|
589
|
+
services a healthcheck, unless the compose file already set one. The web
|
|
590
|
+
image becomes ``${SPLENT_IMAGE:-<product>:latest}``, so a host can run the
|
|
591
|
+
image a release published instead of building its own.
|
|
557
592
|
|
|
558
593
|
Applied by product:build to the merged deploy compose, so every product
|
|
559
594
|
gets it on its next build, whatever its docker-compose.prod.yml says and
|
|
@@ -570,6 +605,11 @@ def apply_production_policies(compose_data: dict, product: str) -> dict:
|
|
|
570
605
|
)
|
|
571
606
|
if web_name and isinstance(services[web_name], dict):
|
|
572
607
|
services[web_name].setdefault("healthcheck", dict(WEB_HEALTHCHECK))
|
|
608
|
+
# The local build, unless .env.deploy names a registry image. Only the
|
|
609
|
+
# plain local name is rewritten: an image the product chose on purpose
|
|
610
|
+
# is left alone.
|
|
611
|
+
if services[web_name].get("image") == f"{product}:latest":
|
|
612
|
+
services[web_name]["image"] = local_or_registry_image(product)
|
|
573
613
|
|
|
574
614
|
db_name = next(
|
|
575
615
|
(n for n in (f"{product}_db_deploy", f"{product}_db") if n in services),
|
|
@@ -1109,10 +1149,7 @@ def product_build(no_image, skip_preflight):
|
|
|
1109
1149
|
# share the product network, so the prod service cannot simply take the
|
|
1110
1150
|
# dev name as an alias. The CLI owns both names, so it owns the mapping.
|
|
1111
1151
|
for k, v in env_result.items():
|
|
1112
|
-
|
|
1113
|
-
env_result[k] = v.replace(
|
|
1114
|
-
"__PRODUCT___web", f"{product}_web_deploy"
|
|
1115
|
-
).replace("__PRODUCT__", product)
|
|
1152
|
+
env_result[k] = resolve_product_placeholders(v, product, prod=True)
|
|
1116
1153
|
|
|
1117
1154
|
# Remove __FEATURE_HOST_DIR__ variables — not needed in prod
|
|
1118
1155
|
# (templates are baked into Docker images, no bind mounts)
|
|
@@ -13,7 +13,12 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
13
13
|
|
|
14
14
|
from splent_cli.utils.path_utils import PathUtils
|
|
15
15
|
from splent_cli.services import context, spl_store
|
|
16
|
-
from splent_cli.utils.template_drift import
|
|
16
|
+
from splent_cli.utils.template_drift import (
|
|
17
|
+
DEFAULT_UPGRADE_SCHEDULE,
|
|
18
|
+
is_reserved_repo_name,
|
|
19
|
+
not_a_product_reason,
|
|
20
|
+
)
|
|
21
|
+
from splent_cli.utils.template_drift import _forge_host as _template_forge_host
|
|
17
22
|
|
|
18
23
|
try:
|
|
19
24
|
from importlib.metadata import version as _pkg_version
|
|
@@ -218,6 +223,10 @@ def make_product(name, spl_name, spl_doi, features_file, force):
|
|
|
218
223
|
# Dropping it here made every product created this way fall back to
|
|
219
224
|
# "<name>.uvl" and 404, with nothing local left that knew the real name.
|
|
220
225
|
"spl_file": pin.file or "",
|
|
226
|
+
# The forge workflows. The same values product:sync-template --ci
|
|
227
|
+
# renders, so a new product does not start out drifted.
|
|
228
|
+
"forge_host": _template_forge_host(),
|
|
229
|
+
"upgrade_schedule": DEFAULT_UPGRADE_SCHEDULE,
|
|
221
230
|
# Production persistence. These switch on the named-volume mounts in
|
|
222
231
|
# the prod compose template so uploads survive redeploys. Products
|
|
223
232
|
# scaffolded before these variables existed render without them and
|
|
@@ -304,6 +313,8 @@ def _write_product(env, name, base_path, template_ctx):
|
|
|
304
313
|
"scripts/05_0_start_app_dev.sh": "product/product_05_0_start_app_dev.sh.j2",
|
|
305
314
|
"scripts/05_1_start_app_prod.sh": "product/product_05_1_start_app_prod.sh.j2",
|
|
306
315
|
".gitignore": "product/product_.gitignore.j2",
|
|
316
|
+
".forgejo/workflows/splent-validate.yml": "product/product_ci_validate.yml.j2",
|
|
317
|
+
".forgejo/workflows/splent-upgrade.yml": "product/product_ci_upgrade.yml.j2",
|
|
307
318
|
"LICENSE": "product/product_LICENSE.j2",
|
|
308
319
|
"package.json": "product/product_package.json.j2",
|
|
309
320
|
"pyproject.toml": "product/product_pyproject.toml.j2",
|
|
@@ -441,9 +441,18 @@ def product_deploy(down, ci):
|
|
|
441
441
|
)
|
|
442
442
|
continue
|
|
443
443
|
if ci:
|
|
444
|
-
|
|
444
|
+
# The environment first. Without it, whatever the interactive
|
|
445
|
+
# deploy would offer as a default is taken (a random string
|
|
446
|
+
# for a secret, the conventional database names), so a feature
|
|
447
|
+
# that ships a <SET> secret does not stop an unattended deploy.
|
|
448
|
+
# What only the operator knows still has to come from them.
|
|
449
|
+
env_value = os.getenv(key) or suggest_value(key, product)
|
|
445
450
|
if env_value:
|
|
446
451
|
env_vars[key] = env_value
|
|
452
|
+
if not os.getenv(key):
|
|
453
|
+
click.echo(
|
|
454
|
+
click.style(" secret ", dim=True) + f"{key} generated"
|
|
455
|
+
)
|
|
447
456
|
else:
|
|
448
457
|
missing_vars.append(key)
|
|
449
458
|
else:
|
|
@@ -624,6 +633,41 @@ def product_deploy(down, ci):
|
|
|
624
633
|
# container starts.
|
|
625
634
|
compose.ensure_network(compose.network_name(product))
|
|
626
635
|
|
|
636
|
+
# A registry image (SPLENT_IMAGE) is pulled first: Compose only pulls an
|
|
637
|
+
# image it does not have, so a host that already ran an older build of
|
|
638
|
+
# the same tag would otherwise redeploy that one.
|
|
639
|
+
image = (env_vars.get("SPLENT_IMAGE") or "").strip()
|
|
640
|
+
if "/" in image:
|
|
641
|
+
click.echo(click.style(" image ", dim=True) + f"pulling {image}")
|
|
642
|
+
pulled = subprocess.run(
|
|
643
|
+
[
|
|
644
|
+
"docker",
|
|
645
|
+
"compose",
|
|
646
|
+
"-p",
|
|
647
|
+
compose.deploy_project_name(product),
|
|
648
|
+
"-f",
|
|
649
|
+
compose_path,
|
|
650
|
+
"--env-file",
|
|
651
|
+
env_path,
|
|
652
|
+
"pull",
|
|
653
|
+
f"{product}_web_deploy",
|
|
654
|
+
],
|
|
655
|
+
capture_output=True,
|
|
656
|
+
text=True,
|
|
657
|
+
cwd=docker_dir,
|
|
658
|
+
env={**os.environ, **host_docker_dir_env(workspace, product)},
|
|
659
|
+
)
|
|
660
|
+
if pulled.returncode != 0:
|
|
661
|
+
click.secho(f" Could not pull {image}.", fg="red")
|
|
662
|
+
detail = (pulled.stderr or pulled.stdout or "").strip().splitlines()
|
|
663
|
+
for line in detail[-5:]:
|
|
664
|
+
click.echo(f" {line}")
|
|
665
|
+
click.echo(
|
|
666
|
+
" Check SPLENT_IMAGE in docker/.env.deploy, and docker login to "
|
|
667
|
+
"that registry when the image is private."
|
|
668
|
+
)
|
|
669
|
+
raise SystemExit(1)
|
|
670
|
+
|
|
627
671
|
click.echo(click.style(" deploying ", dim=True) + f"{product} (prod)...")
|
|
628
672
|
|
|
629
673
|
try:
|
|
@@ -368,11 +368,18 @@ def product_env(generate, merge, env_name, process_all):
|
|
|
368
368
|
# second product of the same line came up.
|
|
369
369
|
merged["SPLENT_NETWORK"] = compose.network_name(product)
|
|
370
370
|
|
|
371
|
-
# Replace __PRODUCT__ placeholder with the actual product name
|
|
371
|
+
# Replace __PRODUCT__ placeholder with the actual product name.
|
|
372
|
+
# product_build imports this module, so the helper is imported here.
|
|
373
|
+
from splent_cli.commands.product.product_build import (
|
|
374
|
+
resolve_product_placeholders,
|
|
375
|
+
)
|
|
376
|
+
|
|
372
377
|
product_replaced = []
|
|
373
378
|
for k, v in merged.items():
|
|
374
379
|
if "__PRODUCT__" in v:
|
|
375
|
-
merged[k] =
|
|
380
|
+
merged[k] = resolve_product_placeholders(
|
|
381
|
+
v, product, prod=env_name == "prod"
|
|
382
|
+
)
|
|
376
383
|
product_replaced.append(k)
|
|
377
384
|
|
|
378
385
|
if env_name == "prod":
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
|
-
product:release
|
|
2
|
+
product:release, a product release: version bump, tag, forge, PyPI and the image.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
5
7
|
import os
|
|
6
8
|
import subprocess
|
|
9
|
+
import tempfile
|
|
7
10
|
|
|
8
11
|
import click
|
|
9
12
|
from splent_cli.commands.product.product_build import (
|
|
10
13
|
build_production_image,
|
|
11
14
|
production_dockerfile,
|
|
12
15
|
)
|
|
13
|
-
from splent_cli.services import context, release, release_gate
|
|
16
|
+
from splent_cli.services import context, image_registry, release, release_gate
|
|
14
17
|
from splent_cli.utils.proc import run, require_docker
|
|
15
18
|
|
|
16
19
|
|
|
@@ -51,19 +54,73 @@ def _guard_all_features_pinned(product_path: str):
|
|
|
51
54
|
raise SystemExit(1)
|
|
52
55
|
|
|
53
56
|
|
|
54
|
-
def
|
|
55
|
-
"""
|
|
57
|
+
def release_targets(product: str, version: str, product_path: str, org: str):
|
|
58
|
+
"""The registries this release pushes the image to, from the product's pyproject."""
|
|
59
|
+
try:
|
|
60
|
+
registries, mirrors = image_registry.declared_registries(
|
|
61
|
+
os.path.join(product_path, "pyproject.toml")
|
|
62
|
+
)
|
|
63
|
+
except image_registry.RegistryDeclarationError as e:
|
|
64
|
+
click.secho(f" error: {e}", fg="red")
|
|
65
|
+
raise SystemExit(1)
|
|
66
|
+
return image_registry.image_targets(product, version, org, registries, mirrors)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#: Docker Hub answers to these names in a client's configuration.
|
|
70
|
+
_DOCKER_HUB_HOSTS = ("index.docker.io", "registry-1.docker.io", "docker.io")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@contextlib.contextmanager
|
|
74
|
+
def registry_session(hosts=()):
|
|
75
|
+
"""A docker client configuration of its own for one release's logins and pushes.
|
|
76
|
+
|
|
77
|
+
The host's configuration may hand credentials to a helper, the macOS
|
|
78
|
+
keychain under Docker Desktop, and that helper refuses to store a login
|
|
79
|
+
for a host the keychain already holds a git password for ("The specified
|
|
80
|
+
item already exists in the keychain"), which stopped the marketplace's
|
|
81
|
+
first release. A configuration of its own also keeps the release tokens
|
|
82
|
+
off the machine: it is deleted when the release is done.
|
|
83
|
+
"""
|
|
84
|
+
# An empty credsStore is not enough: the docker client then detects the
|
|
85
|
+
# platform's helper again. An empty helper per host is what makes it keep
|
|
86
|
+
# the login in this file.
|
|
87
|
+
helpers = {host: "" for host in (*hosts, *_DOCKER_HUB_HOSTS) if host}
|
|
88
|
+
with tempfile.TemporaryDirectory(prefix="splent-docker-") as config_dir:
|
|
89
|
+
with open(os.path.join(config_dir, "config.json"), "w") as handle:
|
|
90
|
+
json.dump({"auths": {}, "credHelpers": helpers}, handle)
|
|
91
|
+
yield {**os.environ, "DOCKER_CONFIG": config_dir}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _login(target, env=None) -> bool:
|
|
95
|
+
user, secret = target.credentials()
|
|
96
|
+
if not user or not secret:
|
|
97
|
+
return False
|
|
98
|
+
command = ["docker", "login", "-u", user, "--password-stdin"]
|
|
99
|
+
if target.host:
|
|
100
|
+
command.insert(2, target.host)
|
|
101
|
+
try:
|
|
102
|
+
run(command, input=secret, capture=True, env=env)
|
|
103
|
+
except subprocess.CalledProcessError:
|
|
104
|
+
return False
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def release_docker_image(product: str, version: str, docker_dir: str, targets):
|
|
109
|
+
"""Build the production image once and push it to every registry.
|
|
56
110
|
|
|
57
111
|
Public because release:resume has to be able to finish this channel too:
|
|
58
112
|
the image is pushed last, after the forge and PyPI are already
|
|
59
113
|
irreversible.
|
|
60
114
|
|
|
61
115
|
The image is the one product:build makes, docker/Dockerfile.<product>.prod
|
|
62
|
-
built with the workspace as context, so what
|
|
63
|
-
deploy runs.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
built with the workspace as context, so what a registry serves is what a
|
|
117
|
+
deploy runs. build_production_image stages the SPL's UVL model first, so
|
|
118
|
+
the pushed image ships the model the product pins even from a clean clone
|
|
119
|
+
where product:build never ran.
|
|
120
|
+
|
|
121
|
+
Each registry gets the version tag and the ``stable`` channel tag (Docker
|
|
122
|
+
Hub also ``latest``). A required registry that refuses the login or the
|
|
123
|
+
push stops the release here; a mirror that does is reported and skipped.
|
|
67
124
|
"""
|
|
68
125
|
require_docker()
|
|
69
126
|
|
|
@@ -76,50 +133,52 @@ def release_docker_image(product: str, version: str, docker_dir: str):
|
|
|
76
133
|
)
|
|
77
134
|
raise SystemExit(1)
|
|
78
135
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
with registry_session([t.host for t in targets]) as docker_env:
|
|
137
|
+
usable = []
|
|
138
|
+
for target in targets:
|
|
139
|
+
click.echo(f" docker logging into {target.label}...")
|
|
140
|
+
if _login(target, docker_env):
|
|
141
|
+
usable.append(target)
|
|
142
|
+
continue
|
|
143
|
+
if target.mirror:
|
|
144
|
+
click.secho(
|
|
145
|
+
f" docker the {target.label} mirror is left out, its login "
|
|
146
|
+
f"failed or {' / '.join(target.credential_names())} are not set",
|
|
147
|
+
fg="yellow",
|
|
148
|
+
)
|
|
149
|
+
continue
|
|
150
|
+
user_env, secret_env = target.credential_names()
|
|
151
|
+
click.secho(
|
|
152
|
+
f" error: could not log into {target.label}. Check {user_env} and "
|
|
153
|
+
f"{secret_env}.",
|
|
154
|
+
fg="red",
|
|
155
|
+
)
|
|
156
|
+
raise SystemExit(1)
|
|
157
|
+
|
|
158
|
+
refs = [ref for target in usable for ref in target.refs]
|
|
159
|
+
click.echo(" docker building image...")
|
|
160
|
+
try:
|
|
161
|
+
build_production_image(product, str(context.workspace()), docker_dir, refs)
|
|
162
|
+
except subprocess.CalledProcessError:
|
|
163
|
+
click.secho(" error: Docker image build failed.", fg="red")
|
|
164
|
+
raise SystemExit(1)
|
|
165
|
+
|
|
166
|
+
for target in usable:
|
|
167
|
+
click.echo(f" docker pushing to {target.label}...")
|
|
168
|
+
try:
|
|
169
|
+
for ref in target.refs:
|
|
170
|
+
run(["docker", "push", ref], env=docker_env)
|
|
171
|
+
except subprocess.CalledProcessError:
|
|
172
|
+
if target.mirror:
|
|
173
|
+
click.secho(
|
|
174
|
+
f" docker the push to the {target.label} mirror failed "
|
|
175
|
+
"and is left out",
|
|
176
|
+
fg="yellow",
|
|
177
|
+
)
|
|
178
|
+
continue
|
|
179
|
+
click.secho(f" error: the push to {target.label} failed.", fg="red")
|
|
180
|
+
raise SystemExit(1)
|
|
181
|
+
click.echo(f" docker {', '.join(target.refs)}")
|
|
123
182
|
|
|
124
183
|
click.echo(" docker push complete")
|
|
125
184
|
|
|
@@ -139,7 +198,7 @@ def _left_out_flags(channels) -> list[str]:
|
|
|
139
198
|
|
|
140
199
|
@click.command(
|
|
141
200
|
"product:release",
|
|
142
|
-
short_help="Release a product: version bump, tag, forge
|
|
201
|
+
short_help="Release a product: version bump, tag, forge, PyPI and image registries.",
|
|
143
202
|
)
|
|
144
203
|
@click.argument("version", required=False, default=None)
|
|
145
204
|
@click.option("--product", default=None, help="Override SPLENT_APP.")
|
|
@@ -210,7 +269,7 @@ def product_release(
|
|
|
210
269
|
raise SystemExit(1)
|
|
211
270
|
org, repo_name = parts
|
|
212
271
|
|
|
213
|
-
#
|
|
272
|
+
# The image is a third channel, and it is the one that publishes LAST, so
|
|
214
273
|
# it is the one most able to diverge. It goes through the gate with the
|
|
215
274
|
# others instead of being discovered at the end.
|
|
216
275
|
channels = release.resolve_channels(
|
|
@@ -250,7 +309,12 @@ def product_release(
|
|
|
250
309
|
|
|
251
310
|
def _docker_hook(_path, ver):
|
|
252
311
|
if release_gate.DOCKER in channels:
|
|
253
|
-
release_docker_image(
|
|
312
|
+
release_docker_image(
|
|
313
|
+
product,
|
|
314
|
+
ver,
|
|
315
|
+
docker_dir,
|
|
316
|
+
release_targets(product, ver, product_path, org),
|
|
317
|
+
)
|
|
254
318
|
|
|
255
319
|
release.run_release_pipeline(
|
|
256
320
|
product,
|
|
@@ -262,8 +326,9 @@ def product_release(
|
|
|
262
326
|
channels=channels,
|
|
263
327
|
resume_target=resume_target,
|
|
264
328
|
allow_unfinished=allow_unfinished,
|
|
265
|
-
|
|
266
|
-
|
|
329
|
+
image_targets=release_targets(product, version, product_path, org)
|
|
330
|
+
if release_gate.DOCKER in channels
|
|
331
|
+
else None,
|
|
267
332
|
post_pypi_hook=_docker_hook,
|
|
268
333
|
wait=wait,
|
|
269
334
|
)
|
{splent_cli-1.28.2 → splent_cli-1.29.1}/src/splent_cli/commands/product/product_sync_template.py
RENAMED
|
@@ -37,6 +37,12 @@ from splent_cli.utils.template_drift import (
|
|
|
37
37
|
is_flag=True,
|
|
38
38
|
help="Update only docker/ files.",
|
|
39
39
|
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--ci",
|
|
42
|
+
"only_ci",
|
|
43
|
+
is_flag=True,
|
|
44
|
+
help="Update only the forge workflows in .forgejo/workflows/.",
|
|
45
|
+
)
|
|
40
46
|
@click.option(
|
|
41
47
|
"--dry-run",
|
|
42
48
|
is_flag=True,
|
|
@@ -48,6 +54,7 @@ def product_sync_template(
|
|
|
48
54
|
only_scripts,
|
|
49
55
|
only_entrypoints,
|
|
50
56
|
only_docker,
|
|
57
|
+
only_ci,
|
|
51
58
|
dry_run,
|
|
52
59
|
yes,
|
|
53
60
|
):
|
|
@@ -60,6 +67,7 @@ def product_sync_template(
|
|
|
60
67
|
splent product:sync-template myapp
|
|
61
68
|
splent product:sync-template myapp --scripts
|
|
62
69
|
splent product:sync-template myapp --docker
|
|
70
|
+
splent product:sync-template myapp --ci
|
|
63
71
|
splent product:sync-template myapp --dry-run
|
|
64
72
|
splent product:sync-template myapp --yes
|
|
65
73
|
"""
|
|
@@ -72,7 +80,7 @@ def product_sync_template(
|
|
|
72
80
|
product_path = resolve_product_dir(workspace, product_name)
|
|
73
81
|
|
|
74
82
|
# Select which groups to process
|
|
75
|
-
filter_active = only_scripts or only_entrypoints or only_docker
|
|
83
|
+
filter_active = only_scripts or only_entrypoints or only_docker or only_ci
|
|
76
84
|
groups_to_run: dict[str, dict] = {}
|
|
77
85
|
if not filter_active or only_scripts:
|
|
78
86
|
groups_to_run["scripts"] = PRODUCT_GROUPS["scripts"]
|
|
@@ -80,6 +88,8 @@ def product_sync_template(
|
|
|
80
88
|
groups_to_run["entrypoints"] = PRODUCT_GROUPS["entrypoints"]
|
|
81
89
|
if not filter_active or only_docker:
|
|
82
90
|
groups_to_run["docker"] = PRODUCT_GROUPS["docker"]
|
|
91
|
+
if not filter_active or only_ci:
|
|
92
|
+
groups_to_run["ci"] = PRODUCT_GROUPS["ci"]
|
|
83
93
|
|
|
84
94
|
ctx = product_ctx(product_name)
|
|
85
95
|
|