mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-15 19:22:36 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3460d8a3ea | ||
|
|
8dabc27272 | ||
|
|
5e08f1d712 |
@@ -137,8 +137,8 @@ CLI available as `cf` or `compose-farm`.
|
||||
| `down` | Stop stacks (`docker compose down`). Use `--orphaned` to stop stacks removed from config |
|
||||
| `stop` | Stop services without removing containers (`docker compose stop`) |
|
||||
| `pull` | Pull latest images |
|
||||
| `restart` | `down` + `up -d` |
|
||||
| `update` | `pull` + `build` + `down` + `up -d` |
|
||||
| `restart` | Restart running containers (`docker compose restart`) |
|
||||
| `update` | Pull, build, recreate only if changed (`up -d --pull always --build`) |
|
||||
| `apply` | Make reality match config: migrate stacks + stop orphans. Use `--dry-run` to preview |
|
||||
| `compose` | Run any docker compose command on a stack (passthrough) |
|
||||
| `logs` | Show stack logs |
|
||||
|
||||
23
README.md
23
README.md
@@ -369,8 +369,8 @@ The CLI is available as both `compose-farm` and the shorter `cf` alias.
|
||||
| `cf up <stack>` | Start stack (auto-migrates if host changed) |
|
||||
| `cf down <stack>` | Stop and remove stack containers |
|
||||
| `cf stop <stack>` | Stop stack without removing containers |
|
||||
| `cf restart <stack>` | down + up |
|
||||
| `cf update <stack>` | pull + build + down + up |
|
||||
| `cf restart <stack>` | Restart running containers |
|
||||
| `cf update <stack>` | Pull, build, recreate only if changed |
|
||||
| `cf pull <stack>` | Pull latest images |
|
||||
| `cf logs -f <stack>` | Follow logs |
|
||||
| `cf ps` | Show status of all stacks |
|
||||
@@ -400,10 +400,10 @@ cf down --orphaned # stop stacks removed from config
|
||||
# Pull latest images
|
||||
cf pull --all
|
||||
|
||||
# Restart (down + up)
|
||||
# Restart running containers
|
||||
cf restart plex
|
||||
|
||||
# Update (pull + build + down + up) - the end-to-end update command
|
||||
# Update (pull + build, only recreates containers if images changed)
|
||||
cf update --all
|
||||
|
||||
# Update state from reality (discovers running stacks + captures digests)
|
||||
@@ -473,10 +473,8 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
│ stop Stop services without removing containers (docker compose │
|
||||
│ stop). │
|
||||
│ pull Pull latest images (docker compose pull). │
|
||||
│ restart Restart stacks (down + up). With --service, restarts just │
|
||||
│ that service. │
|
||||
│ update Update stacks (pull + build + down + up). With --service, │
|
||||
│ updates just that service. │
|
||||
│ restart Restart running containers (docker compose restart). │
|
||||
│ update Update stacks. Only recreates containers if images changed. │
|
||||
│ apply Make reality match config (start, migrate, stop │
|
||||
│ strays/orphans as needed). │
|
||||
│ compose Run any docker compose command on a stack. │
|
||||
@@ -659,7 +657,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
|
||||
Usage: cf restart [OPTIONS] [STACKS]...
|
||||
|
||||
Restart stacks (down + up). With --service, restarts just that service.
|
||||
Restart running containers (docker compose restart).
|
||||
|
||||
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
|
||||
│ stacks [STACKS]... Stacks to operate on │
|
||||
@@ -694,8 +692,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
|
||||
Usage: cf update [OPTIONS] [STACKS]...
|
||||
|
||||
Update stacks (pull + build + down + up). With --service, updates just that
|
||||
service.
|
||||
Update stacks. Only recreates containers if images changed.
|
||||
|
||||
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
|
||||
│ stacks [STACKS]... Stacks to operate on │
|
||||
@@ -1283,12 +1280,12 @@ published ports.
|
||||
|
||||
**Auto-regeneration**
|
||||
|
||||
To automatically regenerate the Traefik config after `up`, `down`, `restart`, or `update`,
|
||||
To automatically regenerate the Traefik config after `up`, `down`, or `update`,
|
||||
add `traefik_file` to your config:
|
||||
|
||||
```yaml
|
||||
compose_dir: /opt/compose
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml # auto-regenerate on up/down/restart/update
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml # auto-regenerate on up/down/update
|
||||
traefik_stack: traefik # skip stacks on same host (docker provider handles them)
|
||||
|
||||
hosts:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
compose_dir: /opt/compose
|
||||
|
||||
# Optional: Auto-regenerate Traefik file-provider config after up/down/restart/update
|
||||
# Optional: Auto-regenerate Traefik file-provider config after up/down/update
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml
|
||||
traefik_stack: traefik # Skip stacks on same host (docker provider handles them)
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ The Compose Farm CLI is available as both `compose-farm` and the shorter alias `
|
||||
| | `up` | Start stacks |
|
||||
| | `down` | Stop stacks |
|
||||
| | `stop` | Stop services without removing containers |
|
||||
| | `restart` | Restart stacks (down + up) |
|
||||
| | `update` | Update stacks (pull + build + down + up) |
|
||||
| | `restart` | Restart running containers |
|
||||
| | `update` | Update stacks (only recreates if images changed) |
|
||||
| | `pull` | Pull latest images |
|
||||
| | `compose` | Run any docker compose command |
|
||||
| **Monitoring** | `ps` | Show stack status |
|
||||
@@ -197,7 +197,7 @@ cf stop immich --service database
|
||||
|
||||
### cf restart
|
||||
|
||||
Restart stacks (down + up). With `--service`, restarts just that service.
|
||||
Restart running containers (`docker compose restart`). With `--service`, restarts just that service.
|
||||
|
||||
```bash
|
||||
cf restart [OPTIONS] [STACKS]...
|
||||
@@ -225,7 +225,7 @@ cf restart immich --service database
|
||||
|
||||
### cf update
|
||||
|
||||
Update stacks (pull + build + down + up). With `--service`, updates just that service.
|
||||
Update stacks. Only recreates containers if images changed. With `--service`, updates just that service.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/update.webm" type="video/webm">
|
||||
|
||||
@@ -107,7 +107,7 @@ Supported compose file names (checked in order):
|
||||
|
||||
### traefik_file
|
||||
|
||||
Path to auto-generated Traefik file-provider config. When set, Compose Farm regenerates this file after `up`, `down`, `restart`, and `update` commands.
|
||||
Path to auto-generated Traefik file-provider config. When set, Compose Farm regenerates this file after `up`, `down`, and `update` commands.
|
||||
|
||||
```yaml
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Update Demo
|
||||
# Shows updating stacks (pull + build + down + up)
|
||||
# Shows updating stacks (only recreates containers if images changed)
|
||||
|
||||
Output docs/assets/update.gif
|
||||
Output docs/assets/update.webm
|
||||
|
||||
@@ -329,7 +329,7 @@ cf apply
|
||||
|
||||
```bash
|
||||
cf update --all
|
||||
# Runs: pull + build + down + up for each stack
|
||||
# Only recreates containers if images changed
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -139,7 +139,6 @@ stacks:
|
||||
With `traefik_file` set, these commands auto-regenerate the config:
|
||||
- `cf up`
|
||||
- `cf down`
|
||||
- `cf restart`
|
||||
- `cf update`
|
||||
- `cf apply`
|
||||
|
||||
|
||||
@@ -168,4 +168,4 @@ traefik_file: /opt/stacks/traefik/dynamic.d/compose-farm.yml
|
||||
traefik_stack: traefik
|
||||
```
|
||||
|
||||
With `traefik_file` configured, compose-farm automatically regenerates the config after `up`, `down`, `restart`, and `update` commands.
|
||||
With `traefik_file` configured, compose-farm automatically regenerates the config after `up`, `down`, and `update` commands.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
compose_dir: /opt/stacks/compose-farm/examples
|
||||
|
||||
# Auto-regenerate Traefik file-provider config after up/down/restart/update
|
||||
# Auto-regenerate Traefik file-provider config after up/down/update
|
||||
traefik_file: /opt/stacks/compose-farm/examples/traefik/dynamic.d/compose-farm.yml
|
||||
traefik_stack: traefik # Skip Traefik's host in file-provider (docker provider handles it)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Annotated, TypeVar
|
||||
|
||||
@@ -69,21 +68,6 @@ ServiceOption = Annotated[
|
||||
_MISSING_PATH_PREVIEW_LIMIT = 2
|
||||
_STATS_PREVIEW_LIMIT = 3 # Max number of pending migrations to show by name
|
||||
|
||||
# Environment variable to identify the web stack (for self-update ordering)
|
||||
CF_WEB_STACK = os.environ.get("CF_WEB_STACK", "")
|
||||
|
||||
|
||||
def sort_web_stack_last(stacks: list[str]) -> list[str]:
|
||||
"""Move the web stack to the end of the list.
|
||||
|
||||
When updating all stacks, the web UI stack (compose-farm) should be updated
|
||||
last. Otherwise, the container restarts mid-process and cancels remaining
|
||||
updates. The CF_WEB_STACK env var identifies the web stack.
|
||||
"""
|
||||
if CF_WEB_STACK and CF_WEB_STACK in stacks:
|
||||
return [s for s in stacks if s != CF_WEB_STACK] + [CF_WEB_STACK]
|
||||
return stacks
|
||||
|
||||
|
||||
def format_host(host: str | list[str]) -> str:
|
||||
"""Format a host value for display."""
|
||||
|
||||
@@ -23,13 +23,12 @@ from compose_farm.cli.common import (
|
||||
maybe_regenerate_traefik,
|
||||
report_results,
|
||||
run_async,
|
||||
sort_web_stack_last,
|
||||
validate_host_for_stack,
|
||||
validate_stacks,
|
||||
)
|
||||
from compose_farm.cli.management import _discover_stacks_full
|
||||
from compose_farm.console import MSG_DRY_RUN, console, print_error, print_success
|
||||
from compose_farm.executor import run_compose_on_host, run_on_stacks, run_sequential_on_stacks
|
||||
from compose_farm.executor import run_compose_on_host, run_on_stacks
|
||||
from compose_farm.operations import (
|
||||
stop_orphaned_stacks,
|
||||
stop_stray_stacks,
|
||||
@@ -162,21 +161,17 @@ def restart(
|
||||
service: ServiceOption = None,
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
"""Restart stacks (down + up). With --service, restarts just that service."""
|
||||
"""Restart running containers (docker compose restart)."""
|
||||
stack_list, cfg = get_stacks(stacks or [], all_stacks, config)
|
||||
if service:
|
||||
if len(stack_list) != 1:
|
||||
print_error("--service requires exactly one stack")
|
||||
raise typer.Exit(1)
|
||||
# For service-level restart, use docker compose restart (more efficient)
|
||||
raw = True
|
||||
results = run_async(run_on_stacks(cfg, stack_list, f"restart {service}", raw=raw))
|
||||
cmd = f"restart {service}"
|
||||
else:
|
||||
# Sort web stack last to avoid self-restart canceling remaining restarts
|
||||
stack_list = sort_web_stack_last(stack_list)
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(run_sequential_on_stacks(cfg, stack_list, ["down", "up -d"], raw=raw))
|
||||
maybe_regenerate_traefik(cfg, results)
|
||||
cmd = "restart"
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(run_on_stacks(cfg, stack_list, cmd, raw=raw))
|
||||
report_results(results)
|
||||
|
||||
|
||||
@@ -187,36 +182,17 @@ def update(
|
||||
service: ServiceOption = None,
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
"""Update stacks (pull + build + down + up). With --service, updates just that service."""
|
||||
"""Update stacks. Only recreates containers if images changed."""
|
||||
stack_list, cfg = get_stacks(stacks or [], all_stacks, config)
|
||||
if service:
|
||||
if len(stack_list) != 1:
|
||||
print_error("--service requires exactly one stack")
|
||||
raise typer.Exit(1)
|
||||
# For service-level update: pull + build + stop + up (stop instead of down)
|
||||
raw = True
|
||||
results = run_async(
|
||||
run_sequential_on_stacks(
|
||||
cfg,
|
||||
stack_list,
|
||||
[
|
||||
f"pull --ignore-buildable {service}",
|
||||
f"build {service}",
|
||||
f"stop {service}",
|
||||
f"up -d {service}",
|
||||
],
|
||||
raw=raw,
|
||||
)
|
||||
)
|
||||
cmd = f"up -d --pull always --build {service}"
|
||||
else:
|
||||
# Sort web stack last to avoid self-restart canceling remaining updates
|
||||
stack_list = sort_web_stack_last(stack_list)
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(
|
||||
run_sequential_on_stacks(
|
||||
cfg, stack_list, ["pull --ignore-buildable", "build", "down", "up -d"], raw=raw
|
||||
)
|
||||
)
|
||||
cmd = "up -d --pull always --build"
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(run_on_stacks(cfg, stack_list, cmd, raw=raw))
|
||||
maybe_regenerate_traefik(cfg, results)
|
||||
report_results(results)
|
||||
|
||||
@@ -333,11 +309,6 @@ def apply( # noqa: C901, PLR0912, PLR0915 (multi-phase reconciliation needs the
|
||||
console.print(f"\n{MSG_DRY_RUN}")
|
||||
return
|
||||
|
||||
# Sort web stack last in each phase to avoid self-restart canceling remaining work
|
||||
migrations = sort_web_stack_last(migrations)
|
||||
missing = sort_web_stack_last(missing)
|
||||
to_refresh = sort_web_stack_last(to_refresh)
|
||||
|
||||
# Execute changes
|
||||
console.print()
|
||||
all_results = []
|
||||
|
||||
@@ -76,7 +76,7 @@ stacks:
|
||||
# traefik_file: (optional) Auto-generate Traefik file-provider config
|
||||
# ------------------------------------------------------------------------------
|
||||
# When set, compose-farm automatically regenerates this file after
|
||||
# up/down/restart/update commands. Traefik watches this file for changes.
|
||||
# up/down/update commands. Traefik watches this file for changes.
|
||||
#
|
||||
# traefik_file: /opt/compose/traefik/dynamic.d/compose-farm.yml
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -14,6 +15,9 @@ if TYPE_CHECKING:
|
||||
from compose_farm.web.deps import get_config
|
||||
from compose_farm.web.streaming import run_cli_streaming, run_compose_streaming, tasks
|
||||
|
||||
# Environment variable to identify the web stack (for exclusion from bulk updates)
|
||||
CF_WEB_STACK = os.environ.get("CF_WEB_STACK", "")
|
||||
|
||||
router = APIRouter(tags=["actions"])
|
||||
|
||||
# Store task references to prevent garbage collection
|
||||
@@ -96,7 +100,15 @@ async def pull_all() -> dict[str, Any]:
|
||||
|
||||
@router.post("/update-all")
|
||||
async def update_all() -> dict[str, Any]:
|
||||
"""Update all stacks (pull + build + down + up)."""
|
||||
"""Update all stacks, excluding the web stack. Only recreates if images changed.
|
||||
|
||||
The web stack is excluded to prevent the UI from shutting down mid-operation.
|
||||
Use 'cf update <web-stack>' manually to update the web UI.
|
||||
"""
|
||||
config = get_config()
|
||||
task_id = _start_task(lambda tid: run_cli_streaming(config, ["update", "--all"], tid))
|
||||
return {"task_id": task_id, "command": "update --all"}
|
||||
# Get all stacks except the web stack to avoid self-shutdown
|
||||
stacks = [s for s in config.stacks if s != CF_WEB_STACK]
|
||||
if not stacks:
|
||||
return {"task_id": "", "command": "update (no stacks)", "skipped": True}
|
||||
task_id = _start_task(lambda tid: run_cli_streaming(config, ["update", *stacks], tid))
|
||||
return {"task_id": task_id, "command": f"update {' '.join(stacks)}"}
|
||||
|
||||
@@ -608,7 +608,7 @@ function playFabIntro() {
|
||||
cmd('action', 'Apply', 'Make reality match config', dashboardAction('apply'), icons.check),
|
||||
cmd('action', 'Refresh', 'Update state from reality', dashboardAction('refresh'), icons.refresh_cw),
|
||||
cmd('action', 'Pull All', 'Pull latest images for all stacks', dashboardAction('pull-all'), icons.cloud_download),
|
||||
cmd('action', 'Update All', 'Update all stacks', dashboardAction('update-all'), icons.refresh_cw),
|
||||
cmd('action', 'Update All', 'Update all stacks except web', dashboardAction('update-all'), icons.refresh_cw),
|
||||
cmd('app', 'Theme', 'Change color theme', openThemePicker, icons.palette),
|
||||
cmd('app', 'Dashboard', 'Go to dashboard', nav('/'), icons.home),
|
||||
cmd('app', 'Live Stats', 'View all containers across hosts', nav('/live-stats'), icons.box),
|
||||
@@ -628,7 +628,7 @@ function playFabIntro() {
|
||||
stackCmd('Down', 'Stop', 'down', icons.square),
|
||||
stackCmd('Restart', 'Restart', 'restart', icons.rotate_cw),
|
||||
stackCmd('Pull', 'Pull', 'pull', icons.cloud_download),
|
||||
stackCmd('Update', 'Pull + restart', 'update', icons.refresh_cw),
|
||||
stackCmd('Update', 'Pull + recreate', 'update', icons.refresh_cw),
|
||||
stackCmd('Logs', 'View logs for', 'logs', icons.file_text),
|
||||
);
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ def _is_self_update(stack: str, command: str) -> bool:
|
||||
"""
|
||||
if not CF_WEB_STACK or stack != CF_WEB_STACK:
|
||||
return False
|
||||
# Commands that involve 'down' need SSH: update, restart, down
|
||||
return command in ("update", "restart", "down")
|
||||
# Commands that involve 'down' need SSH: update, down
|
||||
return command in ("update", "down")
|
||||
|
||||
|
||||
async def _run_cli_via_ssh(
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{{ action_btn("Apply", "/api/apply", "primary", "Make reality match config", check()) }}
|
||||
{{ action_btn("Refresh", "/api/refresh", "outline", "Update state from reality", refresh_cw()) }}
|
||||
{{ action_btn("Pull All", "/api/pull-all", "outline", "Pull latest images for all stacks", cloud_download()) }}
|
||||
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks (pull + build + down + up)", rotate_cw()) }}
|
||||
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks except web (only recreates if changed)", rotate_cw()) }}
|
||||
<div class="tooltip" data-tip="Save compose-farm.yaml config file"><button id="save-config-btn" class="btn btn-outline">{{ save() }} Save Config</button></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<!-- Lifecycle -->
|
||||
{{ action_btn("Up", "/api/stack/" ~ name ~ "/up", "primary", "Start stack (docker compose up -d)", play()) }}
|
||||
{{ action_btn("Down", "/api/stack/" ~ name ~ "/down", "outline", "Stop stack (docker compose down)", square()) }}
|
||||
{{ action_btn("Restart", "/api/stack/" ~ name ~ "/restart", "secondary", "Restart stack (down + up)", rotate_cw()) }}
|
||||
{{ action_btn("Update", "/api/stack/" ~ name ~ "/update", "accent", "Update to latest (pull + build + down + up)", download()) }}
|
||||
{{ action_btn("Restart", "/api/stack/" ~ name ~ "/restart", "secondary", "Restart running containers", rotate_cw()) }}
|
||||
{{ action_btn("Update", "/api/stack/" ~ name ~ "/update", "accent", "Update to latest (only recreates if changed)", download()) }}
|
||||
|
||||
<div class="divider divider-horizontal mx-0"></div>
|
||||
|
||||
|
||||
@@ -437,71 +437,3 @@ class TestDownOrphaned:
|
||||
)
|
||||
|
||||
assert exc_info.value.exit_code == 1
|
||||
|
||||
|
||||
class TestSortWebStackLast:
|
||||
"""Tests for the sort_web_stack_last helper."""
|
||||
|
||||
def test_no_web_stack_env(self) -> None:
|
||||
"""When CF_WEB_STACK is not set, list is unchanged."""
|
||||
from compose_farm.cli import common
|
||||
|
||||
original = common.CF_WEB_STACK
|
||||
try:
|
||||
common.CF_WEB_STACK = ""
|
||||
stacks = ["a", "b", "c"]
|
||||
result = common.sort_web_stack_last(stacks)
|
||||
assert result == ["a", "b", "c"]
|
||||
finally:
|
||||
common.CF_WEB_STACK = original
|
||||
|
||||
def test_web_stack_not_in_list(self) -> None:
|
||||
"""When web stack is not in list, list is unchanged."""
|
||||
from compose_farm.cli import common
|
||||
|
||||
original = common.CF_WEB_STACK
|
||||
try:
|
||||
common.CF_WEB_STACK = "webstack"
|
||||
stacks = ["a", "b", "c"]
|
||||
result = common.sort_web_stack_last(stacks)
|
||||
assert result == ["a", "b", "c"]
|
||||
finally:
|
||||
common.CF_WEB_STACK = original
|
||||
|
||||
def test_web_stack_moved_to_end(self) -> None:
|
||||
"""When web stack is in list, it's moved to the end."""
|
||||
from compose_farm.cli import common
|
||||
|
||||
original = common.CF_WEB_STACK
|
||||
try:
|
||||
common.CF_WEB_STACK = "webstack"
|
||||
stacks = ["a", "webstack", "b", "c"]
|
||||
result = common.sort_web_stack_last(stacks)
|
||||
assert result == ["a", "b", "c", "webstack"]
|
||||
finally:
|
||||
common.CF_WEB_STACK = original
|
||||
|
||||
def test_web_stack_already_last(self) -> None:
|
||||
"""When web stack is already last, list is unchanged."""
|
||||
from compose_farm.cli import common
|
||||
|
||||
original = common.CF_WEB_STACK
|
||||
try:
|
||||
common.CF_WEB_STACK = "webstack"
|
||||
stacks = ["a", "b", "webstack"]
|
||||
result = common.sort_web_stack_last(stacks)
|
||||
assert result == ["a", "b", "webstack"]
|
||||
finally:
|
||||
common.CF_WEB_STACK = original
|
||||
|
||||
def test_empty_list(self) -> None:
|
||||
"""Empty list returns empty list."""
|
||||
from compose_farm.cli import common
|
||||
|
||||
original = common.CF_WEB_STACK
|
||||
try:
|
||||
common.CF_WEB_STACK = "webstack"
|
||||
result = common.sort_web_stack_last([])
|
||||
assert result == []
|
||||
finally:
|
||||
common.CF_WEB_STACK = original
|
||||
|
||||
@@ -98,19 +98,18 @@ class TestMigrationCommands:
|
||||
class TestUpdateCommandSequence:
|
||||
"""Tests for update command sequence."""
|
||||
|
||||
def test_update_command_sequence_includes_build(self) -> None:
|
||||
"""Update command should use pull --ignore-buildable and build."""
|
||||
def test_update_command_uses_pull_always_and_build(self) -> None:
|
||||
"""Update command should use --pull always --build flags."""
|
||||
# This is a static check of the command sequence in lifecycle.py
|
||||
# The actual command sequence is defined in the update function
|
||||
|
||||
source = inspect.getsource(lifecycle.update)
|
||||
|
||||
# Verify the command sequence includes pull --ignore-buildable
|
||||
assert "pull --ignore-buildable" in source
|
||||
# Verify build is included
|
||||
assert '"build"' in source or "'build'" in source
|
||||
# Verify the sequence is pull, build, down, up
|
||||
assert "down" in source
|
||||
# Verify the command uses --pull always (only recreates if image changed)
|
||||
assert "--pull always" in source
|
||||
# Verify --build is included for buildable services
|
||||
assert "--build" in source
|
||||
# Verify up -d is used
|
||||
assert "up -d" in source
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user