bg-coolify-migrate 2.2.0__tar.gz → 2.2.2__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 (123) hide show
  1. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/CHANGELOG.md +26 -0
  2. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/PKG-INFO +3 -3
  3. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/README.MD +2 -2
  4. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/__init__.py +1 -1
  5. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/api/resources.py +25 -0
  6. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/steps.py +19 -11
  7. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_api_resources.py +51 -0
  8. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_engine_steps.py +40 -0
  9. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/.gitignore +0 -0
  10. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/LICENSE +0 -0
  11. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/README.template.MD +0 -0
  12. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/SECURITY.template.MD +0 -0
  13. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/architecture.md +0 -0
  14. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/cli.md +0 -0
  15. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/configuration.md +0 -0
  16. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/index.md +0 -0
  17. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/installation.md +0 -0
  18. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/quickstart.md +0 -0
  19. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/safety.md +0 -0
  20. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/server-migration.md +0 -0
  21. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/troubleshooting.md +0 -0
  22. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/docs/why.md +0 -0
  23. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/pyproject.toml +0 -0
  24. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/api/__init__.py +0 -0
  25. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/api/client.py +0 -0
  26. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/api/fields.py +0 -0
  27. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/cli.py +0 -0
  28. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/discovery/__init__.py +0 -0
  29. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/discovery/docker.py +0 -0
  30. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/discovery/quiesce.py +0 -0
  31. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/dns/__init__.py +0 -0
  32. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/dns/extract.py +0 -0
  33. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/dns/gate.py +0 -0
  34. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/dns/resolve.py +0 -0
  35. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/__init__.py +0 -0
  36. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/compose.py +0 -0
  37. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/drift.py +0 -0
  38. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/images.py +0 -0
  39. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/kinds.py +0 -0
  40. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/manifest.py +0 -0
  41. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/naming.py +0 -0
  42. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/plan.py +0 -0
  43. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/domain/statemachine.py +0 -0
  44. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/__init__.py +0 -0
  45. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/compensations.py +0 -0
  46. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/context.py +0 -0
  47. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/executor.py +0 -0
  48. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/keys.py +0 -0
  49. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/planner.py +0 -0
  50. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/engine/runner.py +0 -0
  51. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/errors.py +0 -0
  52. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/journal/__init__.py +0 -0
  53. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/journal/store.py +0 -0
  54. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/observability/__init__.py +0 -0
  55. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/observability/logging_setup.py +0 -0
  56. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/py.typed +0 -0
  57. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/__init__.py +0 -0
  58. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/appkey.py +0 -0
  59. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/fencing.py +0 -0
  60. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/inventory.py +0 -0
  61. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/runner.py +0 -0
  62. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/statemachine.py +0 -0
  63. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/server/steps.py +0 -0
  64. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/settings/__init__.py +0 -0
  65. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/settings/base.py +0 -0
  66. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/transfer/__init__.py +0 -0
  67. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/transfer/partition.py +0 -0
  68. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/transfer/rsync.py +0 -0
  69. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/transfer/ssh.py +0 -0
  70. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/transfer/verify.py +0 -0
  71. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/__init__.py +0 -0
  72. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/console.py +0 -0
  73. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/dashboard.py +0 -0
  74. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/report.py +0 -0
  75. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/run_report.py +0 -0
  76. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/server_report.py +0 -0
  77. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/src/bg_coolify_migrate/ui/wizard.py +0 -0
  78. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/__init__.py +0 -0
  79. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/conftest.py +0 -0
  80. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/README.md +0 -0
  81. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/__init__.py +0 -0
  82. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/bootstrap.py +0 -0
  83. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/conftest.py +0 -0
  84. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/docker-compose.yml +0 -0
  85. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/prepare.py +0 -0
  86. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/runner.Dockerfile +0 -0
  87. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_all_engines.py +0 -0
  88. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_compose_service.py +0 -0
  89. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_drift_gate.py +0 -0
  90. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_label_contract.py +0 -0
  91. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_multi_resource.py +0 -0
  92. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_real_migration.py +0 -0
  93. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/e2e/test_rollback.py +0 -0
  94. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/integration/__init__.py +0 -0
  95. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/integration/docker-compose.yml +0 -0
  96. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/integration/prepare.py +0 -0
  97. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/integration/test_real_transfer.py +0 -0
  98. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_api_client.py +0 -0
  99. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_api_fields.py +0 -0
  100. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_cli.py +0 -0
  101. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_discovery.py +0 -0
  102. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_dns.py +0 -0
  103. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_compose.py +0 -0
  104. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_drift.py +0 -0
  105. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_images.py +0 -0
  106. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_kinds.py +0 -0
  107. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_manifest.py +0 -0
  108. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_naming.py +0 -0
  109. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_plan.py +0 -0
  110. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_domain_statemachine.py +0 -0
  111. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_engine.py +0 -0
  112. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_engine_context.py +0 -0
  113. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_engine_planner.py +0 -0
  114. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_engine_wiring.py +0 -0
  115. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_journal.py +0 -0
  116. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_observability.py +0 -0
  117. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_packaging.py +0 -0
  118. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_server_migration.py +0 -0
  119. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_transfer.py +0 -0
  120. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_ui.py +0 -0
  121. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_ui_run.py +0 -0
  122. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_ui_wizard.py +0 -0
  123. {bg_coolify_migrate-2.2.0 → bg_coolify_migrate-2.2.2}/tests/test_verify_collect.py +0 -0
@@ -1,6 +1,32 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.2.2 (2026-07-16)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **engine**: Do not abort quiesce on a resource with no volumes
9
+ ([`03e580e`](https://github.com/bauer-group/IP-CoolifyMigration/commit/03e580e0bee3eb0d6d831c57063b4e1bb742b265))
10
+
11
+ ### Documentation
12
+
13
+ - Update README.MD [automated]
14
+ ([`3a298d5`](https://github.com/bauer-group/IP-CoolifyMigration/commit/3a298d5fa2cc1e388e486f87455c27ae883000fe))
15
+
16
+
17
+ ## v2.2.1 (2026-07-16)
18
+
19
+ ### Bug Fixes
20
+
21
+ - **api**: Rebuild the git URL when recreating a public app
22
+ ([`467c53d`](https://github.com/bauer-group/IP-CoolifyMigration/commit/467c53d4c13ca5e49693ac507fed051f868bc6e2))
23
+
24
+ ### Documentation
25
+
26
+ - Update README.MD [automated]
27
+ ([`a41bdca`](https://github.com/bauer-group/IP-CoolifyMigration/commit/a41bdca77c9a13912c9acf750a51c494802ac6bb))
28
+
29
+
4
30
  ## v2.2.0 (2026-07-16)
5
31
 
6
32
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bg-coolify-migrate
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: BAUER GROUP Coolify migration toolkit — failsafe, resumable project and instance migration with byte-exact volume mirroring
5
5
  Project-URL: Homepage, https://github.com/bauer-group/IP-CoolifyMigration
6
6
  Project-URL: Source, https://github.com/bauer-group/IP-CoolifyMigration
@@ -55,7 +55,7 @@ Requires-Dist: mkdocstrings[python]>=0.26.0; extra == 'docs'
55
55
  Description-Content-Type: text/markdown
56
56
 
57
57
  <!-- AUTO-GENERATED FILE. DO NOT EDIT. Edit docs/README.template.MD instead. -->
58
- <!-- Generated on 2026-07-16 15:57:04 UTC -->
58
+ <!-- Generated on 2026-07-16 17:04:54 UTC -->
59
59
 
60
60
  # BAUER GROUP - Coolify Migration Toolkit
61
61
 
@@ -78,7 +78,7 @@ external orchestrator has none of those constraints. That is what this is.
78
78
 
79
79
  ## Repository Information
80
80
 
81
- - **Version:** v2.1.5
81
+ - **Version:** v2.2.1
82
82
  - **Repository:** bauer-group/IP-CoolifyMigration
83
83
  - **Branch:** main
84
84
  - **Architecture:** Pure-logic cores with thin IO shells; saga engine with a
@@ -1,5 +1,5 @@
1
1
  <!-- AUTO-GENERATED FILE. DO NOT EDIT. Edit docs/README.template.MD instead. -->
2
- <!-- Generated on 2026-07-16 15:57:04 UTC -->
2
+ <!-- Generated on 2026-07-16 17:04:54 UTC -->
3
3
 
4
4
  # BAUER GROUP - Coolify Migration Toolkit
5
5
 
@@ -22,7 +22,7 @@ external orchestrator has none of those constraints. That is what this is.
22
22
 
23
23
  ## Repository Information
24
24
 
25
- - **Version:** v2.1.5
25
+ - **Version:** v2.2.1
26
26
  - **Repository:** bauer-group/IP-CoolifyMigration
27
27
  - **Branch:** main
28
28
  - **Architecture:** Pure-logic cores with thin IO shells; saga engine with a
@@ -32,7 +32,7 @@ from __future__ import annotations
32
32
 
33
33
  from typing import TYPE_CHECKING, Any
34
34
 
35
- __version__ = "2.2.0"
35
+ __version__ = "2.2.2"
36
36
 
37
37
  # Name -> submodule, resolved on first attribute access.
38
38
  _LAZY: dict[str, str] = {
@@ -223,6 +223,27 @@ async def create_service(
223
223
  return str(uuid)
224
224
 
225
225
 
226
+ _GIT_URL_PREFIXES = ("https://", "http://", "git://", "git@")
227
+
228
+
229
+ def _public_git_url(git_repository: str) -> str:
230
+ """A full git URL for the ``/applications/public`` route.
231
+
232
+ Coolify stores a PUBLIC app's repo as the short ``owner/repo`` form: on create
233
+ it parses the URL and keeps only the path (``ApplicationsController`` does
234
+ ``git_repository = segment(1)/segment(2)`` and sets the default GitHub source).
235
+ But the same endpoint validates ``git_repository`` with ``ValidGitRepositoryUrl``
236
+ — it must be a full URL. So a public app that GETs back as ``owner/repo`` is
237
+ rejected on re-create with "must start with https://…". We rebuild the
238
+ github.com URL (Coolify's default public source); anything already a URL is left
239
+ untouched. The private-github-app / deploy-key routes accept the short form, so
240
+ this is only for ``public``.
241
+ """
242
+ if git_repository.startswith(_GIT_URL_PREFIXES):
243
+ return git_repository
244
+ return f"https://github.com/{git_repository.strip('/')}"
245
+
246
+
226
247
  async def create_application(
227
248
  api: CoolifyClient, snapshot: ResourceSnapshot, placement: Placement, source: dict[str, Any]
228
249
  ) -> str:
@@ -234,6 +255,10 @@ async def create_application(
234
255
  body["name"] = snapshot.name
235
256
  body["instant_deploy"] = False
236
257
 
258
+ # Public git apps come back short (owner/repo) but the public route needs a URL.
259
+ if segment == "public" and body.get("git_repository"):
260
+ body["git_repository"] = _public_git_url(str(body["git_repository"]))
261
+
237
262
  # The FQDN is decided by the DNS gate and the finalize policy, not carried
238
263
  # over. coolify-mover copies it verbatim, producing two resources that claim
239
264
  # the same hostname.
@@ -289,17 +289,25 @@ async def _capture_mounts(ctx: MigrationContext) -> None:
289
289
  name=snapshot.name,
290
290
  )
291
291
  if not containers:
292
- raise PreflightError(
293
- f"{snapshot.name}: no containers found on {ctx.plan.source_server.name} "
294
- "before stopping it",
295
- hint=(
296
- "The stack must be running for its volumes to be discoverable"
297
- "Coolify removes containers when it stops them, so this is the only "
298
- "chance to see what they mounted.\n"
299
- "Either the resource is already stopped (start it, then migrate), or "
300
- "its containers do not carry the labels we filter on."
301
- ),
302
- )
292
+ # No containers is only a problem when there is data to copy. The plan's
293
+ # manifest is the signal: if it found volumes, a running stack is the ONLY
294
+ # chance to capture them, so refuse (silently copying nothing for a
295
+ # stateful resource is the failure we exist to prevent). If it found none
296
+ # a stateless/rebuilt resource, or one that is simply stoppedthere is
297
+ # nothing to capture, so carry on and let it be recreated on the target.
298
+ if resource.manifest.to_migrate:
299
+ raise PreflightError(
300
+ f"{snapshot.name}: has volumes to migrate but no running containers "
301
+ f"on {ctx.plan.source_server.name}",
302
+ hint=(
303
+ "Coolify removes containers when it stops them, so a running "
304
+ "stack is the only chance to see what its volumes mounted. Start "
305
+ "the resource, then migrate. If it IS running, its containers may "
306
+ "not carry the labels we filter on."
307
+ ),
308
+ )
309
+ log.info("quiesce.no_containers_no_volumes", resource=snapshot.name)
310
+ continue
303
311
  mounts = await inspect_all_mounts(ctx.source_host, containers)
304
312
  ctx.pre_stop_mounts[snapshot.uuid] = mounts
305
313
  log.info(
@@ -355,6 +355,57 @@ class TestCreateApplication:
355
355
  with pytest.raises(UnsupportedResource, match="missing required field"):
356
356
  await create_resource(api, snapshot, _placement(), {})
357
357
 
358
+ async def test_public_short_repo_is_rebuilt_into_a_github_url(
359
+ self, api: CoolifyClient, respx_mock: respx.Router
360
+ ) -> None:
361
+ # Coolify stores a public app's repo as owner/repo, but the public create
362
+ # route validates git_repository as a URL (422 otherwise). Reconstruct it.
363
+ import json
364
+
365
+ route = respx_mock.post(f"{BASE}/applications/public").mock(
366
+ return_value=httpx.Response(201, json={"uuid": "a2"})
367
+ )
368
+ snapshot = ResourceSnapshot(
369
+ uuid="a1",
370
+ name="bentopdf",
371
+ collection="applications",
372
+ kind=ResourceKind.APP_GIT_BUILD,
373
+ git_repository="alam00000/bentopdf",
374
+ git_branch="main",
375
+ git_auth=GitAuth.PUBLIC,
376
+ )
377
+ await create_resource(
378
+ api,
379
+ snapshot,
380
+ _placement(),
381
+ {
382
+ "git_repository": "alam00000/bentopdf",
383
+ "git_branch": "main",
384
+ "build_pack": "nixpacks",
385
+ },
386
+ )
387
+ body = json.loads(route.calls[0].request.read())
388
+ assert body["git_repository"] == "https://github.com/alam00000/bentopdf"
389
+
390
+
391
+ class TestPublicGitUrl:
392
+ def test_short_form_becomes_a_github_url(self) -> None:
393
+ from bg_coolify_migrate.api.resources import _public_git_url
394
+
395
+ assert _public_git_url("alam00000/bentopdf") == "https://github.com/alam00000/bentopdf"
396
+ assert _public_git_url("/owner/repo/") == "https://github.com/owner/repo"
397
+
398
+ def test_a_real_url_is_left_untouched(self) -> None:
399
+ from bg_coolify_migrate.api.resources import _public_git_url
400
+
401
+ for url in (
402
+ "https://github.com/o/r",
403
+ "http://example.com/o/r",
404
+ "git://example.com/o/r",
405
+ "git@github.com:o/r.git",
406
+ ):
407
+ assert _public_git_url(url) == url
408
+
358
409
 
359
410
  class TestCopyEnvs:
360
411
  async def test_bulk_upsert_overwrites_generated_secrets(
@@ -119,6 +119,46 @@ async def ctx(tmp_path: Path): # type: ignore[no-untyped-def]
119
119
  await api.aclose()
120
120
 
121
121
 
122
+ class TestCaptureMounts:
123
+ """The 'no containers' guard protects stateful resources from silently copying
124
+ nothing - but a stateless / no-volume resource must migrate, not abort."""
125
+
126
+ async def test_refuses_when_there_are_volumes_but_no_containers(
127
+ self, ctx: MigrationContext
128
+ ) -> None:
129
+ # Default ctx: a COPY_DATA resource with a volume, source `docker ps` empty.
130
+ from bg_coolify_migrate.engine.steps import _capture_mounts
131
+
132
+ with pytest.raises(PreflightError, match="has volumes to migrate"):
133
+ await _capture_mounts(ctx)
134
+
135
+ async def test_proceeds_when_a_no_volume_resource_has_no_containers(
136
+ self, ctx: MigrationContext
137
+ ) -> None:
138
+ # A rebuild app with no volumes and no running containers must NOT abort -
139
+ # there is nothing to capture; it is recreated (rebuilt) on the target.
140
+ from bg_coolify_migrate.engine.steps import _capture_mounts
141
+
142
+ ctx.plan = _plan(
143
+ resources=(
144
+ ResourcePlan(
145
+ snapshot=_snapshot(
146
+ name="alam00000/bentopdf",
147
+ collection="applications",
148
+ kind=ResourceKind.APP_GIT_BUILD,
149
+ engine=None,
150
+ image=None,
151
+ builds=True,
152
+ ),
153
+ strategy=Strategy.REBUILD,
154
+ manifest=VolumeManifest(), # no volumes to migrate
155
+ ),
156
+ )
157
+ )
158
+ await _capture_mounts(ctx) # no raise
159
+ assert ctx.pre_stop_mounts == {} # nothing captured, nothing lost
160
+
161
+
122
162
  class TestPreflight:
123
163
  async def test_passes_a_healthy_setup(
124
164
  self, ctx: MigrationContext, respx_mock: respx.Router