splent-cli 1.28.2__tar.gz → 1.29.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.
Files changed (201) hide show
  1. {splent_cli-1.28.2/src/splent_cli.egg-info → splent_cli-1.29.0}/PKG-INFO +1 -1
  2. {splent_cli-1.28.2 → splent_cli-1.29.0}/pyproject.toml +1 -1
  3. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_build.py +42 -5
  4. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_create.py +12 -1
  5. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_deploy.py +45 -1
  6. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_env.py +9 -2
  7. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_release.py +83 -47
  8. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_sync_template.py +11 -1
  9. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/release/release_resume.py +16 -9
  10. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/tokens_setup.py +4 -1
  11. splent_cli-1.29.0/src/splent_cli/services/image_registry.py +173 -0
  12. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/registry.py +100 -0
  13. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/release.py +10 -6
  14. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/release_gate.py +107 -18
  15. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/git_url.py +21 -3
  16. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/template_drift.py +25 -1
  17. {splent_cli-1.28.2 → splent_cli-1.29.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  18. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  19. {splent_cli-1.28.2 → splent_cli-1.29.0}/LICENSE +0 -0
  20. {splent_cli-1.28.2 → splent_cli-1.29.0}/README.md +0 -0
  21. {splent_cli-1.28.2 → splent_cli-1.29.0}/setup.cfg +0 -0
  22. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/__init__.py +0 -0
  23. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/__main__.py +0 -0
  24. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/cli.py +0 -0
  25. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/__init__.py +0 -0
  26. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  27. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  28. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  29. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  30. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  31. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  32. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  33. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  34. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  35. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/__init__.py +0 -0
  36. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  37. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  38. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  39. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_env.py +0 -0
  40. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_features.py +0 -0
  41. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_github.py +0 -0
  42. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  43. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_product.py +0 -0
  44. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  45. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  46. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  47. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  48. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  49. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  50. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  51. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/command/__init__.py +0 -0
  52. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/command/command_create.py +0 -0
  53. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/coverage.py +0 -0
  54. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_console.py +0 -0
  55. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  56. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  57. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  58. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  59. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  60. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  61. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_status.py +0 -0
  62. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  63. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/doctor.py +0 -0
  64. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/env/env_list.py +0 -0
  65. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/env/env_set.py +0 -0
  66. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/env/env_show.py +0 -0
  67. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/export/__init__.py +0 -0
  68. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  69. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  70. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  71. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  72. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  73. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  74. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  75. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  76. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  77. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  78. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  79. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  80. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  81. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  82. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  83. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  84. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  85. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  86. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  87. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  88. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  89. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  90. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  91. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  92. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  93. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  94. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  95. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  96. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  97. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  98. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  99. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  100. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  101. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  102. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  103. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  104. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  105. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  106. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  107. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  108. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  109. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  110. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/lint.py +0 -0
  111. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/locust.py +0 -0
  112. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  113. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  114. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  115. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  116. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  117. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/__init__.py +0 -0
  118. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  119. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  120. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  121. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_config.py +0 -0
  122. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  123. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_console.py +0 -0
  124. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  125. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  126. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  127. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_down.py +0 -0
  128. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  129. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_list.py +0 -0
  130. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  131. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  132. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_port.py +0 -0
  133. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  134. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  135. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  136. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_run.py +0 -0
  137. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_select.py +0 -0
  138. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  139. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  140. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_test.py +0 -0
  141. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_up.py +0 -0
  142. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  143. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/release/__init__.py +0 -0
  144. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/release/release_core.py +0 -0
  145. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/selenium.py +0 -0
  146. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  147. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  148. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  149. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  150. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  151. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  152. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  153. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  154. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  155. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  156. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  157. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  158. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  159. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  160. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  161. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  162. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  163. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/commands/version.py +0 -0
  164. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/__init__.py +0 -0
  165. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/compose.py +0 -0
  166. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/context.py +0 -0
  167. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/credentials.py +0 -0
  168. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/index_refresh.py +0 -0
  169. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/marketplace.py +0 -0
  170. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/marketplace_api.py +0 -0
  171. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/marketplace_url.py +0 -0
  172. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/preflight.py +0 -0
  173. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/pypi_quota.py +0 -0
  174. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/release_dry_run.py +0 -0
  175. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/services/spl_store.py +0 -0
  176. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/__init__.py +0 -0
  177. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/archetype.py +0 -0
  178. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/cache_utils.py +0 -0
  179. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/command_loader.py +0 -0
  180. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  181. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/db_client.py +0 -0
  182. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/db_utils.py +0 -0
  183. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/decorators.py +0 -0
  184. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  185. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/feature_installer.py +0 -0
  186. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/feature_utils.py +0 -0
  187. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/forge.py +0 -0
  188. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/integrity.py +0 -0
  189. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/io_utils.py +0 -0
  190. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/lifecycle.py +0 -0
  191. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/manifest.py +0 -0
  192. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/path_utils.py +0 -0
  193. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/proc.py +0 -0
  194. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/redact.py +0 -0
  195. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  196. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/toml_edit.py +0 -0
  197. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  198. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  199. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  200. {splent_cli-1.28.2 → splent_cli-1.29.0}/src/splent_cli.egg-info/requires.txt +0 -0
  201. {splent_cli-1.28.2 → splent_cli-1.29.0}/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.28.2
3
+ Version: 1.29.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.28.2"
7
+ version = "1.29.0"
8
8
  description = "SPLENT-CLI is a CLI to be able to work on your development more easily."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -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
- if "__PRODUCT__" in v:
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 is_reserved_repo_name, not_a_product_reason
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
- env_value = os.getenv(key)
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] = v.replace("__PRODUCT__", product)
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,5 +1,5 @@
1
1
  """
2
- product:release — Release a product with version bump, tag, GitHub/PyPI/Docker Hub.
2
+ product:release, a product release: version bump, tag, forge, PyPI and the image.
3
3
  """
4
4
 
5
5
  import os
@@ -10,7 +10,7 @@ from splent_cli.commands.product.product_build import (
10
10
  build_production_image,
11
11
  production_dockerfile,
12
12
  )
13
- from splent_cli.services import context, release, release_gate
13
+ from splent_cli.services import context, image_registry, release, release_gate
14
14
  from splent_cli.utils.proc import run, require_docker
15
15
 
16
16
 
@@ -51,19 +51,48 @@ def _guard_all_features_pinned(product_path: str):
51
51
  raise SystemExit(1)
52
52
 
53
53
 
54
- def release_docker_image(product: str, version: str, docker_dir: str):
55
- """Build the production image and push it to Docker Hub.
54
+ def release_targets(product: str, version: str, product_path: str, org: str):
55
+ """The registries this release pushes the image to, from the product's pyproject."""
56
+ try:
57
+ registries, mirrors = image_registry.declared_registries(
58
+ os.path.join(product_path, "pyproject.toml")
59
+ )
60
+ except image_registry.RegistryDeclarationError as e:
61
+ click.secho(f" error: {e}", fg="red")
62
+ raise SystemExit(1)
63
+ return image_registry.image_targets(product, version, org, registries, mirrors)
64
+
65
+
66
+ def _login(target) -> bool:
67
+ user, secret = target.credentials()
68
+ if not user or not secret:
69
+ return False
70
+ command = ["docker", "login", "-u", user, "--password-stdin"]
71
+ if target.host:
72
+ command.insert(2, target.host)
73
+ try:
74
+ run(command, input=secret, capture=True)
75
+ except subprocess.CalledProcessError:
76
+ return False
77
+ return True
78
+
79
+
80
+ def release_docker_image(product: str, version: str, docker_dir: str, targets):
81
+ """Build the production image once and push it to every registry.
56
82
 
57
83
  Public because release:resume has to be able to finish this channel too:
58
84
  the image is pushed last, after the forge and PyPI are already
59
85
  irreversible.
60
86
 
61
87
  The image is the one product:build makes, docker/Dockerfile.<product>.prod
62
- built with the workspace as context, so what Docker Hub serves is what a
63
- deploy runs. Building ``docker_dir`` with no Dockerfile named would look
64
- for docker/Dockerfile, which products do not have. build_production_image
65
- stages the SPL's UVL model first, so the pushed image ships the model the
66
- product pins even from a clean clone where product:build never ran.
88
+ built with the workspace as context, so what a registry serves is what a
89
+ deploy runs. build_production_image stages the SPL's UVL model first, so
90
+ the pushed image ships the model the product pins even from a clean clone
91
+ where product:build never ran.
92
+
93
+ Each registry gets the version tag and the ``stable`` channel tag (Docker
94
+ Hub also ``latest``). A required registry that refuses the login or the
95
+ push stops the release here; a mirror that does is reported and skipped.
67
96
  """
68
97
  require_docker()
69
98
 
@@ -76,50 +105,51 @@ def release_docker_image(product: str, version: str, docker_dir: str):
76
105
  )
77
106
  raise SystemExit(1)
78
107
 
79
- username = os.getenv("DOCKERHUB_USERNAME")
80
- password = os.getenv("DOCKERHUB_PASSWORD")
81
- missing = [
82
- name
83
- for name, value in (
84
- ("DOCKERHUB_USERNAME", username),
85
- ("DOCKERHUB_PASSWORD", password),
86
- )
87
- if not value
88
- ]
89
- if missing:
108
+ usable = []
109
+ for target in targets:
110
+ click.echo(f" docker logging into {target.label}...")
111
+ if _login(target):
112
+ usable.append(target)
113
+ continue
114
+ if target.mirror:
115
+ click.secho(
116
+ f" docker the {target.label} mirror is left out, its login failed "
117
+ f"or {' / '.join(target.credential_names())} are not set",
118
+ fg="yellow",
119
+ )
120
+ continue
121
+ user_env, secret_env = target.credential_names()
90
122
  click.secho(
91
- " error: missing Docker Hub credentials: "
92
- + ", ".join(missing)
93
- + "\n Set them in your environment (e.g. export DOCKERHUB_USERNAME=... "
94
- "DOCKERHUB_PASSWORD=...) before releasing.",
123
+ f" error: could not log into {target.label}. Check {user_env} and "
124
+ f"{secret_env}.",
95
125
  fg="red",
96
126
  )
97
127
  raise SystemExit(1)
98
128
 
99
- image_name = f"{username}/{product}"
100
-
101
- click.echo(" docker logging into Docker Hub...")
102
- run(
103
- ["docker", "login", "-u", username, "--password-stdin"],
104
- input=password,
105
- capture=True,
106
- )
107
-
129
+ refs = [ref for target in usable for ref in target.refs]
108
130
  click.echo(" docker building image...")
109
131
  try:
110
- build_production_image(
111
- product,
112
- str(context.workspace()),
113
- docker_dir,
114
- [f"{image_name}:{version}", f"{image_name}:latest"],
115
- )
132
+ build_production_image(product, str(context.workspace()), docker_dir, refs)
116
133
  except subprocess.CalledProcessError:
117
134
  click.secho(" error: Docker image build failed.", fg="red")
118
135
  raise SystemExit(1)
119
136
 
120
- click.echo(" docker pushing images...")
121
- run(["docker", "push", f"{image_name}:{version}"])
122
- run(["docker", "push", f"{image_name}:latest"])
137
+ for target in usable:
138
+ click.echo(f" docker pushing to {target.label}...")
139
+ try:
140
+ for ref in target.refs:
141
+ run(["docker", "push", ref])
142
+ except subprocess.CalledProcessError:
143
+ if target.mirror:
144
+ click.secho(
145
+ f" docker the push to the {target.label} mirror failed and is "
146
+ "left out",
147
+ fg="yellow",
148
+ )
149
+ continue
150
+ click.secho(f" error: the push to {target.label} failed.", fg="red")
151
+ raise SystemExit(1)
152
+ click.echo(f" docker {', '.join(target.refs)}")
123
153
 
124
154
  click.echo(" docker push complete")
125
155
 
@@ -139,7 +169,7 @@ def _left_out_flags(channels) -> list[str]:
139
169
 
140
170
  @click.command(
141
171
  "product:release",
142
- short_help="Release a product: version bump, tag, forge/PyPI/Docker Hub.",
172
+ short_help="Release a product: version bump, tag, forge, PyPI and image registries.",
143
173
  )
144
174
  @click.argument("version", required=False, default=None)
145
175
  @click.option("--product", default=None, help="Override SPLENT_APP.")
@@ -210,7 +240,7 @@ def product_release(
210
240
  raise SystemExit(1)
211
241
  org, repo_name = parts
212
242
 
213
- # Docker Hub is a third channel, and it is the one that publishes LAST, so
243
+ # The image is a third channel, and it is the one that publishes LAST, so
214
244
  # it is the one most able to diverge. It goes through the gate with the
215
245
  # others instead of being discovered at the end.
216
246
  channels = release.resolve_channels(
@@ -250,7 +280,12 @@ def product_release(
250
280
 
251
281
  def _docker_hook(_path, ver):
252
282
  if release_gate.DOCKER in channels:
253
- release_docker_image(product, ver, docker_dir)
283
+ release_docker_image(
284
+ product,
285
+ ver,
286
+ docker_dir,
287
+ release_targets(product, ver, product_path, org),
288
+ )
254
289
 
255
290
  release.run_release_pipeline(
256
291
  product,
@@ -262,8 +297,9 @@ def product_release(
262
297
  channels=channels,
263
298
  resume_target=resume_target,
264
299
  allow_unfinished=allow_unfinished,
265
- docker_image=f"{os.getenv('DOCKERHUB_USERNAME', '')}/{product}".lstrip("/")
266
- or None,
300
+ image_targets=release_targets(product, version, product_path, org)
301
+ if release_gate.DOCKER in channels
302
+ else None,
267
303
  post_pypi_hook=_docker_hook,
268
304
  wait=wait,
269
305
  )
@@ -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
 
@@ -350,7 +350,11 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
350
350
  )
351
351
  raise SystemExit(1)
352
352
 
353
- docker_image = _docker_image(label) if release_gate.DOCKER in channels else None
353
+ image_targets = (
354
+ _image_targets(label, normalized, path, repo)
355
+ if release_gate.DOCKER in channels
356
+ else None
357
+ )
354
358
 
355
359
  click.echo()
356
360
  click.secho(f" Resuming {label} {tag}", bold=True)
@@ -377,7 +381,7 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
377
381
  version=normalized,
378
382
  channels=channels,
379
383
  resume=True,
380
- docker_image=docker_image,
384
+ image_targets=image_targets,
381
385
  )
382
386
  release_gate.render(report)
383
387
 
@@ -526,7 +530,8 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
526
530
  if make_release:
527
531
  steps.append(f"create the release for {tag} on the forge")
528
532
  if push_image:
529
- steps.append(f"build and push the Docker image {docker_image}:{normalized}")
533
+ refs = ", ".join(f"{t.repository}:{normalized}" for t in image_targets or [])
534
+ steps.append(f"build and push the Docker image {refs}")
530
535
  if make_snapshot:
531
536
  steps.append(f"create the local snapshot for {tag}")
532
537
 
@@ -670,7 +675,9 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
670
675
  from splent_cli.commands.product.product_release import release_docker_image
671
676
 
672
677
  product = label.split("/")[-1]
673
- release_docker_image(product, normalized, os.path.join(path, "docker"))
678
+ release_docker_image(
679
+ product, normalized, os.path.join(path, "docker"), image_targets
680
+ )
674
681
 
675
682
  if make_snapshot:
676
683
  from splent_cli.commands.feature.feature_release import (
@@ -750,11 +757,11 @@ def _declares_pypi(pyproject: str) -> bool:
750
757
  return release_gate.PYPI in {release_gate.canonical_channel(c) for c in channels}
751
758
 
752
759
 
753
- def _docker_image(label: str) -> str | None:
754
- username = os.getenv("DOCKERHUB_USERNAME")
755
- if not username:
756
- return None
757
- return f"{username}/{label.split('/')[-1]}"
760
+ def _image_targets(label: str, version: str, path: str, repo: str):
761
+ """The registries the product's image goes to, as product:release names them."""
762
+ from splent_cli.commands.product.product_release import release_targets
763
+
764
+ return release_targets(label.split("/")[-1], version, path, repo.split("/")[0])
758
765
 
759
766
 
760
767
  cli_command = release_resume
@@ -43,7 +43,10 @@ def tokens():
43
43
  click.echo(f" 1. Go to {forge.host()} → Settings → Applications")
44
44
  click.echo(" → Access Tokens")
45
45
  click.echo(" 2. Generate new token")
46
- click.echo(" 3. Enable scopes: write:repository and write:organization")
46
+ click.echo(
47
+ " 3. Enable scopes: write:repository, write:organization and "
48
+ "write:package (product images go to the forge registry)"
49
+ )
47
50
  click.echo(" 4. Copy the token (shown only once)")
48
51
  click.echo()
49
52
  click.secho(" Add to your .env:", fg="bright_black")