splent-cli 1.50.0__tar.gz → 1.51.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {splent_cli-1.50.0/src/splent_cli.egg-info → splent_cli-1.51.0}/PKG-INFO +1 -1
- {splent_cli-1.50.0 → splent_cli-1.51.0}/pyproject.toml +1 -1
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_deps.py +43 -3
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_release.py +18 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/release/release_resume.py +27 -1
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/frontend.py +4 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/registry.py +17 -11
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/release.py +16 -1
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/release_gate.py +2 -2
- splent_cli-1.51.0/src/splent_cli/services/source_mirror.py +356 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/COPYING +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/COPYING.LESSER +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/README.md +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/setup.cfg +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_frontend_init.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_npm.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_security.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_build.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_frontend_audit.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_params.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/storage/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/storage/storage_migrate.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/deploy_bundle.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/editable_refresh.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/image_registry.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/marketplace.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/neutral_app.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/own_commands.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/security_scan.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/services/web_container.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_frontend.npmrc.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_frontend_scripts.js.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_services_service.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/licenses/COPYING +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_400.html +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_401.html +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_404.html +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_500.html +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_app.css +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_app.js +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_own.css +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/Dockerfile.frontend +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/package-lock.json +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/package.json +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/splent/build.js +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/splent/legacy.js +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/splent/lint.js +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/splent/plugin.js +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/splent/preset.js +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/toolchain/frontend/splent/singletons.json +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/ci_services.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/ci_workflow.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/deploy_freshness.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/feature_naming.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/feature_params.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/feature_requirements.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/feature_resolution.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/frontend_check.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/frontend_licenses.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/menu_keys.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/migration_lock.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/npm_semver.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/running_version.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/toml_edit.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/uvl_diagnosis.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli/utils/uvl_params.py +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.50.0 → splent_cli-1.51.0}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -83,6 +83,48 @@ def _parse_uvl_deps(uvl_path: str) -> tuple[dict[str, str], dict[str, set[str]]]
|
|
|
83
83
|
# ---------------------------------------------------------------------------
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def _bare_imports(source: str) -> set[str]:
|
|
87
|
+
"""The features an import statement of SOURCE names outside a guard.
|
|
88
|
+
|
|
89
|
+
An import under a try whose handler tolerates ImportError, or under an
|
|
90
|
+
if that asks whether the product has the feature, is how a feature
|
|
91
|
+
reaches an optional one (feature:release reads it so for the contract),
|
|
92
|
+
and the product does without it. A source that does not parse is read
|
|
93
|
+
as text, every import counting.
|
|
94
|
+
"""
|
|
95
|
+
import ast
|
|
96
|
+
|
|
97
|
+
from splent_cli.commands.feature.feature_release import (
|
|
98
|
+
_IMPORT_TOLERANT_HANDLERS,
|
|
99
|
+
_guarded_ranges,
|
|
100
|
+
_imported_names,
|
|
101
|
+
_in_ranges,
|
|
102
|
+
_module_string_constants,
|
|
103
|
+
_presence_guards,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
tree = ast.parse(source)
|
|
108
|
+
except (SyntaxError, ValueError):
|
|
109
|
+
return set(
|
|
110
|
+
re.findall(r"(?:from|import)\s+splent_io\.(splent_feature_\w+)", source)
|
|
111
|
+
)
|
|
112
|
+
guarded = _guarded_ranges(tree, _IMPORT_TOLERANT_HANDLERS)
|
|
113
|
+
present = _presence_guards(tree, _module_string_constants(tree))
|
|
114
|
+
found = set()
|
|
115
|
+
for node in ast.walk(tree):
|
|
116
|
+
if not isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
117
|
+
continue
|
|
118
|
+
for item in _imported_names(node):
|
|
119
|
+
for short in re.findall(r"^splent_io\.splent_feature_(\w+?)(?:\.|$)", item):
|
|
120
|
+
if _in_ranges(node.lineno, guarded) or _in_ranges(
|
|
121
|
+
node.lineno, present.get(short, ())
|
|
122
|
+
):
|
|
123
|
+
continue
|
|
124
|
+
found.add(f"splent_feature_{short}")
|
|
125
|
+
return found
|
|
126
|
+
|
|
127
|
+
|
|
86
128
|
def _scan_feature_imports(
|
|
87
129
|
feature_path: str, feature_name: str, all_packages: set[str]
|
|
88
130
|
) -> tuple[set[str], set[str]]:
|
|
@@ -130,9 +172,7 @@ def _scan_feature_imports(
|
|
|
130
172
|
except (OSError, PermissionError):
|
|
131
173
|
continue
|
|
132
174
|
|
|
133
|
-
for match in
|
|
134
|
-
r"(?:from|import)\s+splent_io\.(splent_feature_\w+)", content
|
|
135
|
-
):
|
|
175
|
+
for match in _bare_imports(content):
|
|
136
176
|
if match != feature_name and match in all_packages:
|
|
137
177
|
python_imports.add(match)
|
|
138
178
|
|
|
@@ -28,6 +28,7 @@ from splent_cli.services import (
|
|
|
28
28
|
image_registry,
|
|
29
29
|
release,
|
|
30
30
|
release_gate,
|
|
31
|
+
source_mirror,
|
|
31
32
|
)
|
|
32
33
|
from splent_cli.utils.proc import run, require_docker
|
|
33
34
|
|
|
@@ -385,6 +386,12 @@ def _left_out_flags(channels) -> list[str]:
|
|
|
385
386
|
is_flag=True,
|
|
386
387
|
help="Do not build or push the Docker image for this version.",
|
|
387
388
|
)
|
|
389
|
+
@click.option(
|
|
390
|
+
"--no-mirror",
|
|
391
|
+
is_flag=True,
|
|
392
|
+
help="Do not publish to [tool.splent.release.source_mirror] now, for when it "
|
|
393
|
+
"is down. splent release:resume does it later.",
|
|
394
|
+
)
|
|
388
395
|
@click.option(
|
|
389
396
|
"--allow-unfinished",
|
|
390
397
|
is_flag=True,
|
|
@@ -403,6 +410,7 @@ def product_release(
|
|
|
403
410
|
no_forge,
|
|
404
411
|
no_github,
|
|
405
412
|
no_docker,
|
|
413
|
+
no_mirror,
|
|
406
414
|
allow_unfinished,
|
|
407
415
|
wait,
|
|
408
416
|
):
|
|
@@ -421,6 +429,7 @@ def product_release(
|
|
|
421
429
|
|
|
422
430
|
_guard_all_features_pinned(product_path)
|
|
423
431
|
_guard_deployable(str(context.workspace()), product, product_path)
|
|
432
|
+
mirror = None if no_mirror else _declared_source_mirror(product_path)
|
|
424
433
|
|
|
425
434
|
# Determine org/repo for the semver wizard
|
|
426
435
|
repo = release.get_repo_from_path(product_path)
|
|
@@ -502,7 +511,16 @@ def product_release(
|
|
|
502
511
|
),
|
|
503
512
|
post_pypi_hook=_docker_hook,
|
|
504
513
|
wait=wait,
|
|
514
|
+
source_mirror=mirror,
|
|
505
515
|
)
|
|
506
516
|
|
|
507
517
|
|
|
518
|
+
def _declared_source_mirror(product_path: str):
|
|
519
|
+
try:
|
|
520
|
+
return source_mirror.declared(os.path.join(product_path, "pyproject.toml"))
|
|
521
|
+
except source_mirror.MirrorDeclarationError as e:
|
|
522
|
+
click.secho(f" error: {e}", fg="red")
|
|
523
|
+
raise SystemExit(1)
|
|
524
|
+
|
|
525
|
+
|
|
508
526
|
cli_command = product_release
|
|
@@ -37,7 +37,7 @@ import tempfile
|
|
|
37
37
|
|
|
38
38
|
import click
|
|
39
39
|
|
|
40
|
-
from splent_cli.services import context, release, release_gate
|
|
40
|
+
from splent_cli.services import context, release, release_gate, source_mirror
|
|
41
41
|
from splent_cli.utils import forge
|
|
42
42
|
from splent_cli.utils.git_url import _non_interactive_env
|
|
43
43
|
|
|
@@ -539,6 +539,27 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
|
|
|
539
539
|
if make_snapshot:
|
|
540
540
|
steps.append(f"create the local snapshot for {tag}")
|
|
541
541
|
|
|
542
|
+
# The product's official repository, once the forge has what it copies.
|
|
543
|
+
mirror = None
|
|
544
|
+
if kind == "product" and release_gate.FORGE in channels:
|
|
545
|
+
try:
|
|
546
|
+
mirror = source_mirror.declared(pyproject)
|
|
547
|
+
if mirror:
|
|
548
|
+
mirror_steps = source_mirror.missing(mirror, path, tag)
|
|
549
|
+
steps.extend(mirror_steps)
|
|
550
|
+
mirror = mirror if mirror_steps else None
|
|
551
|
+
except source_mirror.MirrorDeclarationError as e:
|
|
552
|
+
click.secho(f" error: {e}", fg="red")
|
|
553
|
+
raise SystemExit(1)
|
|
554
|
+
except source_mirror.MirrorUnreachable as e:
|
|
555
|
+
stuck.append(
|
|
556
|
+
release_gate.ChannelStatus(
|
|
557
|
+
source_mirror.CHANNEL,
|
|
558
|
+
summary=f"could not ask {mirror.web_url} what it has ({e})",
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
mirror = None
|
|
562
|
+
|
|
542
563
|
click.echo()
|
|
543
564
|
if not steps:
|
|
544
565
|
if stuck:
|
|
@@ -675,6 +696,11 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
|
|
|
675
696
|
if failure:
|
|
676
697
|
failures.append(f"the release for {tag} on the forge ({failure})")
|
|
677
698
|
|
|
699
|
+
if mirror:
|
|
700
|
+
failure = source_mirror.publish(mirror, path, tag)
|
|
701
|
+
if failure:
|
|
702
|
+
failures.append(f"{tag} on {mirror.web_url} ({failure})")
|
|
703
|
+
|
|
678
704
|
if push_image:
|
|
679
705
|
from splent_cli.commands.product.product_release import release_docker_image
|
|
680
706
|
|
|
@@ -740,6 +740,10 @@ def verify_assets(assets, *, lock=None, production: bool = False) -> list[str]:
|
|
|
740
740
|
if dist.is_dir():
|
|
741
741
|
for path in sorted(dist.rglob("*")):
|
|
742
742
|
name = path.relative_to(dist).as_posix()
|
|
743
|
+
# pip compiles the .py a bundle ships to the browser (a pyodide
|
|
744
|
+
# wrapper) where it installs it: that bytecode is no file of it.
|
|
745
|
+
if "__pycache__" in path.parts:
|
|
746
|
+
continue
|
|
743
747
|
if path.is_file() and name not in listed:
|
|
744
748
|
problems.append(f"assets/dist/{name} is not in the manifest")
|
|
745
749
|
if lock is not None and Path(lock).is_file():
|
|
@@ -402,27 +402,33 @@ def github_create_release(
|
|
|
402
402
|
draft: bool = False,
|
|
403
403
|
prerelease: bool = False,
|
|
404
404
|
timeout: int = 30,
|
|
405
|
+
api_base: str | None = None,
|
|
406
|
+
generate_notes: bool = False,
|
|
405
407
|
) -> ReleaseResult:
|
|
406
|
-
"""Create a release for *tag* on the configured forge
|
|
408
|
+
"""Create a release for *tag* on the configured forge, or at *api_base*.
|
|
407
409
|
|
|
408
410
|
Returns the outcome instead of raising for HTTP failures, so the caller can
|
|
409
411
|
tell "already there" (idempotent rerun) from "refused" (a divergence, since
|
|
410
412
|
the tag exists by the time this runs). Raises :class:`RegistryError` only
|
|
411
413
|
when the forge cannot be reached at all.
|
|
414
|
+
|
|
415
|
+
``generate_notes`` asks GitHub to append what changed since the last release
|
|
416
|
+
to *body*. Only GitHub knows it.
|
|
412
417
|
"""
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
418
|
+
fields = {
|
|
419
|
+
"tag_name": tag,
|
|
420
|
+
"name": name,
|
|
421
|
+
"body": body,
|
|
422
|
+
"draft": draft,
|
|
423
|
+
"prerelease": prerelease,
|
|
424
|
+
}
|
|
425
|
+
if generate_notes:
|
|
426
|
+
fields["generate_release_notes"] = True
|
|
427
|
+
payload = json.dumps(fields).encode()
|
|
422
428
|
headers = _github_headers(token)
|
|
423
429
|
headers["Content-Type"] = "application/json"
|
|
424
430
|
req = _forge_request(
|
|
425
|
-
f"{forge.api_base()}/repos/{org}/{repo}/releases",
|
|
431
|
+
f"{api_base or forge.api_base()}/repos/{org}/{repo}/releases",
|
|
426
432
|
data=payload,
|
|
427
433
|
headers=headers,
|
|
428
434
|
method="POST",
|
|
@@ -16,6 +16,7 @@ import tomllib
|
|
|
16
16
|
import click
|
|
17
17
|
|
|
18
18
|
from splent_cli.services import release_gate
|
|
19
|
+
from splent_cli.services import source_mirror as source_mirror_service
|
|
19
20
|
from splent_cli.utils import forge
|
|
20
21
|
from splent_cli.utils.git_url import _non_interactive_env
|
|
21
22
|
from splent_cli.utils.io_utils import atomic_write
|
|
@@ -1619,6 +1620,7 @@ def run_release_pipeline(
|
|
|
1619
1620
|
post_pypi_hook=None,
|
|
1620
1621
|
wait: bool = False,
|
|
1621
1622
|
neutral: bool = False,
|
|
1623
|
+
source_mirror=None,
|
|
1622
1624
|
):
|
|
1623
1625
|
"""Run the standard release pipeline.
|
|
1624
1626
|
|
|
@@ -1634,7 +1636,8 @@ def run_release_pipeline(
|
|
|
1634
1636
|
7. Commit (local, revertible)
|
|
1635
1637
|
8. Build and upload to PyPI
|
|
1636
1638
|
9. Push the branch, the tag, and create the release on the git host
|
|
1637
|
-
10. (optional)
|
|
1639
|
+
10. (optional) the same on source_mirror, the product's official repository
|
|
1640
|
+
11. (optional) post_pypi_hook, e.g. the snapshot or the Docker image
|
|
1638
1641
|
|
|
1639
1642
|
The order of 8 and 9 is the point. A PyPI version can never be replaced or
|
|
1640
1643
|
removed; a tag that was never pushed costs nothing. Uploading first means a
|
|
@@ -1714,6 +1717,10 @@ def run_release_pipeline(
|
|
|
1714
1717
|
deep=deep,
|
|
1715
1718
|
allow_unfinished=allow_unfinished,
|
|
1716
1719
|
)
|
|
1720
|
+
if source_mirror and to_forge:
|
|
1721
|
+
report.statuses.append(
|
|
1722
|
+
source_mirror_service.check(source_mirror, path, tag)
|
|
1723
|
+
)
|
|
1717
1724
|
release_gate.render(report, header=header)
|
|
1718
1725
|
if not report.ok:
|
|
1719
1726
|
release_gate.refuse(report, resume_target=target)
|
|
@@ -1852,6 +1859,14 @@ def run_release_pipeline(
|
|
|
1852
1859
|
)
|
|
1853
1860
|
if failure:
|
|
1854
1861
|
failures.append(f"the release for {tag} on the git host ({failure})")
|
|
1862
|
+
# Before the image: the version the image reaches production with
|
|
1863
|
+
# must already have its page where the app links to it.
|
|
1864
|
+
if source_mirror:
|
|
1865
|
+
failure = source_mirror_service.publish(source_mirror, path, tag)
|
|
1866
|
+
if failure:
|
|
1867
|
+
failures.append(f"{tag} on {source_mirror.web_url} ({failure})")
|
|
1868
|
+
else:
|
|
1869
|
+
accepted.append(source_mirror.web_url)
|
|
1855
1870
|
else:
|
|
1856
1871
|
create_and_push_tag(path, tag, push=False)
|
|
1857
1872
|
click.secho(
|
|
@@ -195,7 +195,7 @@ def canonical_channel(name) -> str:
|
|
|
195
195
|
return CHANNEL_ALIASES.get(key, key)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
_RELEASE_KEYS = {"channels", "registries", "mirrors"}
|
|
198
|
+
_RELEASE_KEYS = {"channels", "registries", "mirrors", "source_mirror"}
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def declared_channels(pyproject_path: str) -> tuple[str, ...]:
|
|
@@ -234,7 +234,7 @@ def declared_channels(pyproject_path: str) -> tuple[str, ...]:
|
|
|
234
234
|
raise ChannelDeclarationError(
|
|
235
235
|
"[tool.splent.release] has unknown key(s) "
|
|
236
236
|
+ ", ".join(unknown)
|
|
237
|
-
+ ". The keys are 'channels', 'registries' and '
|
|
237
|
+
+ ". The keys are 'channels', 'registries', 'mirrors' and 'source_mirror', so this was "
|
|
238
238
|
"ignored silently before."
|
|
239
239
|
)
|
|
240
240
|
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""A product's official repository, when it is not the forge it releases to.
|
|
2
|
+
|
|
3
|
+
A product lives on the forge, with its CI, its tags and its image registry, and
|
|
4
|
+
its public home can be somewhere else. A release has to reach that home too: the
|
|
5
|
+
page of a version is often linked from the app itself, so an image that reaches
|
|
6
|
+
production before its release exists there links to a 404.
|
|
7
|
+
|
|
8
|
+
[tool.splent.release.source_mirror]
|
|
9
|
+
url = "https://github.com/org/repo"
|
|
10
|
+
branches = ["main", "develop"]
|
|
11
|
+
|
|
12
|
+
A release moves every branch named to the commit of its tag, pushes the tag and
|
|
13
|
+
creates the release there, before the image goes out. Branches only ever move
|
|
14
|
+
forward: a mirror with commits this repository lacks refuses the release at the
|
|
15
|
+
gate, before anything changes.
|
|
16
|
+
|
|
17
|
+
The push goes over SSH, as the forge's does. The API call takes a token, from
|
|
18
|
+
SPLENT_SOURCE_MIRROR_TOKEN, GH_TOKEN, the GitHub CLI's login or GITHUB_TOKEN.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import dataclasses
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import tomllib
|
|
28
|
+
from urllib.parse import urlparse
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
|
|
32
|
+
from splent_cli.services import registry, release_gate
|
|
33
|
+
from splent_cli.utils.git_url import _non_interactive_env
|
|
34
|
+
|
|
35
|
+
CHANNEL = "mirror"
|
|
36
|
+
GITHUB_HOST = "github.com"
|
|
37
|
+
GIT_NETWORK_TIMEOUT = 600
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MirrorDeclarationError(ValueError):
|
|
41
|
+
"""[tool.splent.release.source_mirror] does not say what a release needs."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MirrorUnreachable(Exception):
|
|
45
|
+
"""The mirror could not be asked what it has."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass(frozen=True)
|
|
49
|
+
class SourceMirror:
|
|
50
|
+
host: str
|
|
51
|
+
org: str
|
|
52
|
+
repo: str
|
|
53
|
+
branches: tuple[str, ...]
|
|
54
|
+
push_url: str
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def web_url(self) -> str:
|
|
58
|
+
return f"https://{self.host}/{self.org}/{self.repo}"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_github(self) -> bool:
|
|
62
|
+
return self.host == GITHUB_HOST
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def api_base(self) -> str:
|
|
66
|
+
if self.is_github:
|
|
67
|
+
return "https://api.github.com"
|
|
68
|
+
return f"https://{self.host}/api/v1"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def declared(pyproject_path: str) -> SourceMirror | None:
|
|
72
|
+
"""The mirror the product's pyproject.toml declares, or None."""
|
|
73
|
+
try:
|
|
74
|
+
with open(pyproject_path, "rb") as f:
|
|
75
|
+
data = tomllib.load(f)
|
|
76
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
77
|
+
return None
|
|
78
|
+
section = (
|
|
79
|
+
data.get("tool", {}).get("splent", {}).get("release", {}).get("source_mirror")
|
|
80
|
+
)
|
|
81
|
+
if section is None:
|
|
82
|
+
return None
|
|
83
|
+
if not isinstance(section, dict):
|
|
84
|
+
raise MirrorDeclarationError(
|
|
85
|
+
"[tool.splent.release.source_mirror] must be a table with a url."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
url = section.get("url")
|
|
89
|
+
parsed = urlparse(url) if isinstance(url, str) else None
|
|
90
|
+
parts = [p for p in parsed.path.split("/") if p] if parsed else []
|
|
91
|
+
if not parsed or parsed.scheme != "https" or not parsed.hostname or len(parts) != 2:
|
|
92
|
+
raise MirrorDeclarationError(
|
|
93
|
+
"[tool.splent.release.source_mirror] url must be the repository's web "
|
|
94
|
+
'address, for example url = "https://github.com/org/repo".'
|
|
95
|
+
)
|
|
96
|
+
org, repo = parts[0], parts[1].removesuffix(".git")
|
|
97
|
+
|
|
98
|
+
branches = section.get("branches", ["main"])
|
|
99
|
+
if (
|
|
100
|
+
not isinstance(branches, list)
|
|
101
|
+
or not branches
|
|
102
|
+
or not all(isinstance(b, str) and b.strip() for b in branches)
|
|
103
|
+
):
|
|
104
|
+
raise MirrorDeclarationError(
|
|
105
|
+
"[tool.splent.release.source_mirror] branches must be a list of branch "
|
|
106
|
+
'names, for example branches = ["main"].'
|
|
107
|
+
)
|
|
108
|
+
return SourceMirror(
|
|
109
|
+
host=parsed.hostname,
|
|
110
|
+
org=org,
|
|
111
|
+
repo=repo,
|
|
112
|
+
branches=tuple(dict.fromkeys(b.strip() for b in branches)),
|
|
113
|
+
push_url=f"git@{parsed.hostname}:{org}/{repo}.git",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def token(mirror: SourceMirror) -> str | None:
|
|
118
|
+
"""The token for the mirror's API. Never printed."""
|
|
119
|
+
explicit = os.getenv("SPLENT_SOURCE_MIRROR_TOKEN") or os.getenv("GH_TOKEN")
|
|
120
|
+
if explicit:
|
|
121
|
+
return explicit
|
|
122
|
+
gh = shutil.which("gh")
|
|
123
|
+
if gh:
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
[gh, "auth", "token", "--hostname", mirror.host],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
timeout=15,
|
|
130
|
+
)
|
|
131
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
132
|
+
result = None
|
|
133
|
+
if result is not None and result.returncode == 0 and result.stdout.strip():
|
|
134
|
+
return result.stdout.strip()
|
|
135
|
+
return os.getenv("GITHUB_TOKEN") if mirror.is_github else None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _git(path: str, *args: str) -> subprocess.CompletedProcess:
|
|
139
|
+
return subprocess.run(
|
|
140
|
+
["git", *args],
|
|
141
|
+
cwd=path,
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
env=_non_interactive_env(),
|
|
145
|
+
timeout=GIT_NETWORK_TIMEOUT,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _remote_refs(mirror: SourceMirror, path: str) -> dict[str, str]:
|
|
150
|
+
"""``{ref: sha}`` of the mirror's branches and tags."""
|
|
151
|
+
try:
|
|
152
|
+
result = _git(path, "ls-remote", "--heads", "--tags", mirror.push_url)
|
|
153
|
+
except (OSError, subprocess.TimeoutExpired) as e:
|
|
154
|
+
raise MirrorUnreachable(f"git ls-remote did not finish ({e})") from e
|
|
155
|
+
if result.returncode != 0:
|
|
156
|
+
raise MirrorUnreachable(result.stderr.strip() or "git ls-remote failed")
|
|
157
|
+
refs = {}
|
|
158
|
+
for line in result.stdout.splitlines():
|
|
159
|
+
sha, _, ref = line.partition("\t")
|
|
160
|
+
if ref:
|
|
161
|
+
refs[ref] = sha
|
|
162
|
+
return refs
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _have_commit(mirror: SourceMirror, path: str, sha: str, branch: str) -> bool:
|
|
166
|
+
"""Whether *sha*, the tip of the mirror's *branch*, is here, fetching it if not."""
|
|
167
|
+
if _git(path, "cat-file", "-e", f"{sha}^{{commit}}").returncode == 0:
|
|
168
|
+
return True
|
|
169
|
+
_git(path, "fetch", "--no-tags", mirror.push_url, f"refs/heads/{branch}")
|
|
170
|
+
return _git(path, "cat-file", "-e", f"{sha}^{{commit}}").returncode == 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _is_ancestor(path: str, ancestor: str, descendant: str) -> bool:
|
|
174
|
+
result = _git(path, "merge-base", "--is-ancestor", ancestor, descendant)
|
|
175
|
+
return result.returncode == 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check(mirror: SourceMirror, path: str, tag: str) -> release_gate.ChannelStatus:
|
|
179
|
+
"""Will the mirror take this release? Doubt counts as blocked, as in the gate."""
|
|
180
|
+
st = release_gate.ChannelStatus(CHANNEL)
|
|
181
|
+
|
|
182
|
+
def blocked(summary: str, *hints: str) -> release_gate.ChannelStatus:
|
|
183
|
+
st.summary = f"blocked, {summary}"
|
|
184
|
+
st.hints.extend(hints)
|
|
185
|
+
return st
|
|
186
|
+
|
|
187
|
+
secret = token(mirror)
|
|
188
|
+
if not secret:
|
|
189
|
+
return blocked(
|
|
190
|
+
f"no token for {mirror.host}",
|
|
191
|
+
"Set SPLENT_SOURCE_MIRROR_TOKEN, or log in with the GitHub CLI.",
|
|
192
|
+
)
|
|
193
|
+
try:
|
|
194
|
+
repository = registry.github_json(
|
|
195
|
+
f"{mirror.api_base}/repos/{mirror.org}/{mirror.repo}", secret
|
|
196
|
+
)
|
|
197
|
+
except (registry.RegistryError, ValueError) as e:
|
|
198
|
+
return blocked(f"{mirror.web_url} could not be asked ({e})")
|
|
199
|
+
if repository is None:
|
|
200
|
+
return blocked(f"{mirror.web_url} does not exist, or this token cannot see it")
|
|
201
|
+
if repository.get("archived"):
|
|
202
|
+
return blocked(f"{mirror.web_url} is archived")
|
|
203
|
+
if (repository.get("permissions") or {}).get("push") is False:
|
|
204
|
+
return blocked(f"this token cannot push to {mirror.web_url}")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
refs = _remote_refs(mirror, path)
|
|
208
|
+
except MirrorUnreachable as e:
|
|
209
|
+
return blocked(
|
|
210
|
+
f"git cannot reach {mirror.push_url} ({e})",
|
|
211
|
+
"The push goes over SSH, with the key this machine uses for the host.",
|
|
212
|
+
)
|
|
213
|
+
if f"refs/tags/{tag}" in refs:
|
|
214
|
+
return blocked(
|
|
215
|
+
f"{tag} is already there",
|
|
216
|
+
"Finish that version with splent release:resume, or release another.",
|
|
217
|
+
)
|
|
218
|
+
behind = [
|
|
219
|
+
branch
|
|
220
|
+
for branch in mirror.branches
|
|
221
|
+
if (sha := refs.get(f"refs/heads/{branch}"))
|
|
222
|
+
and not (
|
|
223
|
+
_have_commit(mirror, path, sha, branch) and _is_ancestor(path, sha, "HEAD")
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
if behind:
|
|
227
|
+
names = ", ".join(behind)
|
|
228
|
+
return blocked(
|
|
229
|
+
f"{names} on {mirror.web_url} has commits this repository lacks",
|
|
230
|
+
"A branch only moves forward. Bring them in with "
|
|
231
|
+
f"git fetch {mirror.push_url} {behind[0]} && git merge FETCH_HEAD "
|
|
232
|
+
"(-s ours when this repository's tree is the one that stands).",
|
|
233
|
+
)
|
|
234
|
+
st.ok = True
|
|
235
|
+
st.summary = f"ready, {names_of(mirror)} and the release on {mirror.web_url}"
|
|
236
|
+
return st
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def names_of(mirror: SourceMirror) -> str:
|
|
240
|
+
return ", ".join(mirror.branches)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _branches_to_move(
|
|
244
|
+
mirror: SourceMirror, path: str, commit: str, refs: dict[str, str]
|
|
245
|
+
) -> list[str]:
|
|
246
|
+
"""The branches that do not hold *commit* yet."""
|
|
247
|
+
return [
|
|
248
|
+
branch
|
|
249
|
+
for branch in mirror.branches
|
|
250
|
+
if not (
|
|
251
|
+
(sha := refs.get(f"refs/heads/{branch}"))
|
|
252
|
+
and _have_commit(mirror, path, sha, branch)
|
|
253
|
+
and _is_ancestor(path, commit, sha)
|
|
254
|
+
)
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _local_commit(path: str, tag: str) -> str | None:
|
|
259
|
+
result = _git(
|
|
260
|
+
path, "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}^{{commit}}"
|
|
261
|
+
)
|
|
262
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def missing(mirror: SourceMirror, path: str, tag: str) -> list[str]:
|
|
266
|
+
"""What the mirror still lacks of *tag*, as steps to show. Empty when complete.
|
|
267
|
+
|
|
268
|
+
Raises :class:`MirrorUnreachable` when it cannot tell.
|
|
269
|
+
"""
|
|
270
|
+
refs = _remote_refs(mirror, path)
|
|
271
|
+
commit = _local_commit(path, tag)
|
|
272
|
+
steps = []
|
|
273
|
+
if commit is None:
|
|
274
|
+
move = list(mirror.branches)
|
|
275
|
+
else:
|
|
276
|
+
move = _branches_to_move(mirror, path, commit, refs)
|
|
277
|
+
if move:
|
|
278
|
+
steps.append(f"move {', '.join(move)} on {mirror.web_url} to {tag}")
|
|
279
|
+
if f"refs/tags/{tag}" not in refs:
|
|
280
|
+
steps.append(f"push the tag {tag} to {mirror.web_url}")
|
|
281
|
+
try:
|
|
282
|
+
existing = registry.github_json(
|
|
283
|
+
f"{mirror.api_base}/repos/{mirror.org}/{mirror.repo}/releases/tags/{tag}",
|
|
284
|
+
token(mirror),
|
|
285
|
+
)
|
|
286
|
+
except (registry.RegistryError, ValueError) as e:
|
|
287
|
+
raise MirrorUnreachable(str(e)) from e
|
|
288
|
+
if existing is None:
|
|
289
|
+
steps.append(f"create the release for {tag} on {mirror.web_url}")
|
|
290
|
+
return steps
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _release_failure(result, mirror: SourceMirror) -> str:
|
|
294
|
+
if result.rate_limited:
|
|
295
|
+
return f"{mirror.host} is rate limiting this token (HTTP {result.status})"
|
|
296
|
+
if result.status == 401:
|
|
297
|
+
return f"{mirror.host} rejected the token (HTTP 401)"
|
|
298
|
+
if result.status == 403:
|
|
299
|
+
return f"this token cannot create releases on {mirror.web_url} (HTTP 403)"
|
|
300
|
+
return f"{mirror.host} answered HTTP {result.status} {result.detail}".strip()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def publish(mirror: SourceMirror, path: str, tag: str) -> str | None:
|
|
304
|
+
"""Bring *tag* to the mirror: branches, tag and release.
|
|
305
|
+
|
|
306
|
+
Does only what is missing, so running it again finishes it. Returns ``None``
|
|
307
|
+
on success and the reason on failure, for the caller to report once the
|
|
308
|
+
channels that can still finish have.
|
|
309
|
+
"""
|
|
310
|
+
commit = _local_commit(path, tag)
|
|
311
|
+
if commit is None:
|
|
312
|
+
return f"there is no {tag} here to publish"
|
|
313
|
+
try:
|
|
314
|
+
refs = _remote_refs(mirror, path)
|
|
315
|
+
move = _branches_to_move(mirror, path, commit, refs)
|
|
316
|
+
refspecs = [f"{commit}:refs/heads/{branch}" for branch in move]
|
|
317
|
+
if f"refs/tags/{tag}" not in refs:
|
|
318
|
+
refspecs.append(f"refs/tags/{tag}:refs/tags/{tag}")
|
|
319
|
+
if refspecs:
|
|
320
|
+
# All of it or none of it: a tag without its branches, or the
|
|
321
|
+
# reverse, is the state this exists to avoid.
|
|
322
|
+
pushed = _git(path, "push", "--atomic", mirror.push_url, *refspecs)
|
|
323
|
+
if pushed.returncode != 0:
|
|
324
|
+
return pushed.stderr.strip() or "git push failed"
|
|
325
|
+
except (MirrorUnreachable, OSError, subprocess.TimeoutExpired) as e:
|
|
326
|
+
return str(e)
|
|
327
|
+
if move:
|
|
328
|
+
click.echo(f" mirror {names_of(mirror)} at {tag} on {mirror.web_url}")
|
|
329
|
+
|
|
330
|
+
secret = token(mirror)
|
|
331
|
+
if not secret:
|
|
332
|
+
return f"no token for {mirror.host}"
|
|
333
|
+
try:
|
|
334
|
+
result = registry.github_create_release(
|
|
335
|
+
mirror.org,
|
|
336
|
+
mirror.repo,
|
|
337
|
+
tag=tag,
|
|
338
|
+
name=f"{mirror.repo} {tag}",
|
|
339
|
+
body=(
|
|
340
|
+
"Released with [SPLENT](https://www.splent.io). The `deploy/` "
|
|
341
|
+
"directory of this tag holds what a server needs to run this "
|
|
342
|
+
"version.\n"
|
|
343
|
+
),
|
|
344
|
+
token=secret,
|
|
345
|
+
api_base=mirror.api_base,
|
|
346
|
+
generate_notes=mirror.is_github,
|
|
347
|
+
)
|
|
348
|
+
except registry.RegistryError as e:
|
|
349
|
+
return f"{mirror.host} could not be reached ({e})"
|
|
350
|
+
if result.created:
|
|
351
|
+
click.echo(f" mirror release created {result.url or tag}")
|
|
352
|
+
return None
|
|
353
|
+
if result.already_exists:
|
|
354
|
+
click.secho(f" mirror release for {tag} already exists, kept", fg="yellow")
|
|
355
|
+
return None
|
|
356
|
+
return _release_failure(result, mirror)
|
|
@@ -187,6 +187,7 @@ src/splent_cli/services/release.py
|
|
|
187
187
|
src/splent_cli/services/release_dry_run.py
|
|
188
188
|
src/splent_cli/services/release_gate.py
|
|
189
189
|
src/splent_cli/services/security_scan.py
|
|
190
|
+
src/splent_cli/services/source_mirror.py
|
|
190
191
|
src/splent_cli/services/spl_store.py
|
|
191
192
|
src/splent_cli/services/web_container.py
|
|
192
193
|
src/splent_cli/templates/feature/feature_.gitignore.j2
|
|
File without changes
|