taskfile 0.3.78__tar.gz → 0.3.80__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.
- {taskfile-0.3.78/src/taskfile.egg-info → taskfile-0.3.80}/PKG-INFO +2 -1
- {taskfile-0.3.78 → taskfile-0.3.80}/pyproject.toml +2 -1
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/__init__.py +1 -1
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/api/app.py +55 -28
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/interactive/wizards.py +62 -9
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/main.py +66 -59
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/compose.py +1 -1
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/deploy_recipes.py +73 -50
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/checks.py +34 -567
- taskfile-0.3.80/src/taskfile/diagnostics/checks_deploy.py +171 -0
- taskfile-0.3.80/src/taskfile/diagnostics/checks_infra.py +179 -0
- taskfile-0.3.80/src/taskfile/diagnostics/checks_placeholders.py +161 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/checks_ports.py +71 -37
- taskfile-0.3.80/src/taskfile/diagnostics/checks_registry.py +166 -0
- taskfile-0.3.80/src/taskfile/diagnostics/checks_ssh.py +100 -0
- taskfile-0.3.80/src/taskfile/diagnostics/fixop_adapter.py +113 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/llm_repair.py +27 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/health.py +51 -1
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/models/config.py +49 -48
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/classifier.py +29 -30
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/commands.py +16 -212
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/core.py +23 -13
- taskfile-0.3.80/src/taskfile/runner/failure.py +293 -0
- {taskfile-0.3.78 → taskfile-0.3.80/src/taskfile.egg-info}/PKG-INFO +2 -1
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile.egg-info/SOURCES.txt +6 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile.egg-info/requires.txt +1 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_doctor_decomposition.py +18 -12
- taskfile-0.3.78/src/taskfile/diagnostics/checks_ssh.py +0 -266
- {taskfile-0.3.78 → taskfile-0.3.80}/LICENSE +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/README.md +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/setup.cfg +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/__main__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/addons/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/addons/monitoring.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/addons/postgres.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/addons/redis_addon.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/api/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/api/models.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cache.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/base.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/drone.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/gitea.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/github.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/gitlab.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/jenkins.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cigen/makefile.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cirunner.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/api_cmd.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/auth.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/cache_cmds.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/ci.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/click_compat.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/completion.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/deploy.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/diagnostics.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/docker_cmds.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/e2e_cmd.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/explain_cmd.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/fleet.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/health.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/import_export.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/info_cmd.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/interactive/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/interactive/menu.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/quadlet.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/registry_cmds.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/release.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/setup.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/cli/version.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/converters.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/deploy_utils.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/fixes.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/models.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/diagnostics/report.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/fleet.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/graph.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/importer.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/landing.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/models/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/models/environment.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/models/pipeline.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/models/task.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/notifications.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/parser.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/provisioner.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/quadlet.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/registry.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/error_presenter.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/explainer.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/functions.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/resolver.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/runner/ssh.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/codereview.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/full.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/minimal.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/multiplatform.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/podman.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/publish.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/codereview.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/full.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/iot.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/kubernetes.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/minimal.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/multiplatform.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/podman.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/publish.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/saas.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/terraform.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/templates/web.yml +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/scaffold/web.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/ssh.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/watch.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/webui/__init__.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/webui/dashboard.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/webui/handlers.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile/webui/server.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile.egg-info/dependency_links.txt +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile.egg-info/entry_points.txt +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/src/taskfile.egg-info/top_level.txt +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_api.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_auth.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_cigen.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_classifier.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_cli.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_compose.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_deploy_validation.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_diagnostics.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_docker_e2e.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_doctor_e2e.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_dsl_commands.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_e2e_examples.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_fleet.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_graceful_restart.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_health.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_landing.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_models.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_parser.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_provisioner.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_quadlet.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_release.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_resolver.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_runner.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_scaffold.py +0 -0
- {taskfile-0.3.78 → taskfile-0.3.80}/tests/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskfile
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.80
|
|
4
4
|
Summary: Universal Taskfile runner with multi-environment deploy support. CI/CD agnostic — run locally or from any pipeline.
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,6 +20,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
20
20
|
Requires-Dist: click>=8.0
|
|
21
21
|
Requires-Dist: rich>=13.0
|
|
22
22
|
Requires-Dist: clickmd>=1.1.1
|
|
23
|
+
Requires-Dist: fixop>=0.1.0
|
|
23
24
|
Provides-Extra: ssh
|
|
24
25
|
Requires-Dist: paramiko>=3.0; extra == "ssh"
|
|
25
26
|
Provides-Extra: api
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "taskfile"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.80"
|
|
8
8
|
description = "Universal Taskfile runner with multi-environment deploy support. CI/CD agnostic — run locally or from any pipeline."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
"click>=8.0",
|
|
26
26
|
"rich>=13.0",
|
|
27
27
|
"clickmd>=1.1.1",
|
|
28
|
+
"fixop>=0.1.0",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[project.optional-dependencies]
|
|
@@ -555,6 +555,16 @@ def _register_doctor_routes(app: FastAPI) -> None:
|
|
|
555
555
|
)
|
|
556
556
|
|
|
557
557
|
|
|
558
|
+
# ── Category filter for doctor endpoint ──────────────────────────
|
|
559
|
+
|
|
560
|
+
_CATEGORY_FILTER = {
|
|
561
|
+
"config": {"config_error", "taskfile_bug"},
|
|
562
|
+
"env": {"dependency_missing"},
|
|
563
|
+
"infra": {"external_error"},
|
|
564
|
+
"runtime": {"runtime_error"},
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
558
568
|
def _run_doctor(
|
|
559
569
|
app: FastAPI,
|
|
560
570
|
*,
|
|
@@ -567,37 +577,45 @@ def _run_doctor(
|
|
|
567
577
|
from taskfile.diagnostics import ProjectDiagnostics
|
|
568
578
|
|
|
569
579
|
diagnostics = ProjectDiagnostics()
|
|
570
|
-
|
|
571
580
|
diagnostics.run_all_checks(verbose=verbose)
|
|
572
581
|
|
|
573
|
-
|
|
574
|
-
|
|
582
|
+
fixed_count = _apply_layer4_fixes(diagnostics, fix)
|
|
583
|
+
llm_suggestions = _apply_layer5_llm(diagnostics, llm)
|
|
584
|
+
issues = _filter_issues_by_category(diagnostics._issues, category)
|
|
585
|
+
issue_infos, by_category = _build_issue_infos(issues)
|
|
586
|
+
|
|
587
|
+
return _build_doctor_response(issue_infos, by_category, fixed_count, llm_suggestions)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _apply_layer4_fixes(diagnostics, fix: bool) -> int:
|
|
591
|
+
"""Layer 4: Apply auto-fixes if requested. Returns fixed count."""
|
|
575
592
|
if fix and diagnostics.issues:
|
|
576
|
-
|
|
593
|
+
return diagnostics.auto_fix()
|
|
594
|
+
return 0
|
|
577
595
|
|
|
578
|
-
|
|
579
|
-
|
|
596
|
+
|
|
597
|
+
def _apply_layer5_llm(diagnostics, llm: bool) -> list[str]:
|
|
598
|
+
"""Layer 5: Ask LLM for suggestions on unresolved issues."""
|
|
580
599
|
if llm and diagnostics._issues:
|
|
581
600
|
try:
|
|
582
|
-
|
|
601
|
+
return diagnostics.llm_repair()
|
|
583
602
|
except Exception:
|
|
584
603
|
pass
|
|
604
|
+
return []
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _filter_issues_by_category(issues, category: str):
|
|
608
|
+
"""Filter issues by doctor category (config/env/infra/runtime/all)."""
|
|
609
|
+
if category == "all" or category not in _CATEGORY_FILTER:
|
|
610
|
+
return issues
|
|
611
|
+
allowed = _CATEGORY_FILTER[category]
|
|
612
|
+
return [i for i in issues if i.category.value in allowed]
|
|
585
613
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"runtime": {"runtime_error"},
|
|
592
|
-
}
|
|
593
|
-
issues = diagnostics._issues
|
|
594
|
-
if category != "all" and category in CATEGORY_FILTER:
|
|
595
|
-
allowed = CATEGORY_FILTER[category]
|
|
596
|
-
issues = [i for i in issues if i.category.value in allowed]
|
|
597
|
-
|
|
598
|
-
# Build response
|
|
599
|
-
issue_infos = []
|
|
600
|
-
categories: dict[str, list[DoctorIssueInfo]] = {}
|
|
614
|
+
|
|
615
|
+
def _build_issue_infos(issues) -> tuple[list[DoctorIssueInfo], dict[str, list[DoctorIssueInfo]]]:
|
|
616
|
+
"""Convert Issue objects to API response models, grouped by category."""
|
|
617
|
+
infos: list[DoctorIssueInfo] = []
|
|
618
|
+
by_category: dict[str, list[DoctorIssueInfo]] = {}
|
|
601
619
|
for iss in issues:
|
|
602
620
|
info = DoctorIssueInfo(
|
|
603
621
|
category=iss.category.value,
|
|
@@ -611,13 +629,22 @@ def _run_doctor(
|
|
|
611
629
|
teach=iss.teach,
|
|
612
630
|
context={k: v for k, v in iss.context.items() if not k.startswith("_")} if iss.context else None,
|
|
613
631
|
)
|
|
614
|
-
|
|
615
|
-
|
|
632
|
+
infos.append(info)
|
|
633
|
+
by_category.setdefault(iss.category.value, []).append(info)
|
|
634
|
+
return infos, by_category
|
|
635
|
+
|
|
616
636
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
637
|
+
def _build_doctor_response(
|
|
638
|
+
issue_infos: list[DoctorIssueInfo],
|
|
639
|
+
categories: dict[str, list[DoctorIssueInfo]],
|
|
640
|
+
fixed_count: int,
|
|
641
|
+
llm_suggestions: list[str],
|
|
642
|
+
) -> DoctorResponse:
|
|
643
|
+
"""Assemble the final DoctorResponse from computed parts."""
|
|
644
|
+
error_count = sum(1 for i in issue_infos if i.severity == "error")
|
|
645
|
+
warn_count = sum(1 for i in issue_infos if i.severity == "warning")
|
|
646
|
+
info_count = sum(1 for i in issue_infos if i.severity == "info")
|
|
647
|
+
fixable = sum(1 for i in issue_infos if i.auto_fixable)
|
|
621
648
|
|
|
622
649
|
parts = []
|
|
623
650
|
if error_count:
|
|
@@ -263,13 +263,8 @@ curl -X POST http://localhost:8000/doctor -H "Content-Type: application/json" \
|
|
|
263
263
|
border_style="blue"
|
|
264
264
|
))
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
diagnostics.run_all_checks(verbose=verbose, remote=remote)
|
|
268
|
-
# Remote-only: extended remote diagnostics
|
|
269
|
-
if remote:
|
|
270
|
-
_run_remote_diagnostics(diagnostics)
|
|
266
|
+
_doctor_run_checks(diagnostics, verbose=verbose, remote=remote, report=report)
|
|
271
267
|
|
|
272
|
-
# Validate examples/ directory if requested
|
|
273
268
|
if check_examples_flag:
|
|
274
269
|
_run_examples_check(diagnostics, report)
|
|
275
270
|
|
|
@@ -278,19 +273,32 @@ curl -X POST http://localhost:8000/doctor -H "Content-Type: application/json" \
|
|
|
278
273
|
sys.exit(1 if any(s == "error" for _, s, _ in diagnostics.issues) else 0)
|
|
279
274
|
|
|
280
275
|
diagnostics.print_report(show_teach=teach)
|
|
276
|
+
_doctor_apply_fixes(diagnostics, fix=fix, llm=llm)
|
|
277
|
+
_doctor_print_summary(diagnostics)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _doctor_run_checks(diagnostics: ProjectDiagnostics, *, verbose: bool, remote: bool, report: bool) -> None:
|
|
281
|
+
"""Layers 1-3: Run all diagnostic checks (with optional spinner)."""
|
|
282
|
+
with console.status("[bold green]Checking project...[/]" if not remote else "[bold green]Checking remote server...[/]") if not report else _nullcontext():
|
|
283
|
+
diagnostics.run_all_checks(verbose=verbose, remote=remote)
|
|
284
|
+
if remote:
|
|
285
|
+
_run_remote_diagnostics(diagnostics)
|
|
286
|
+
|
|
281
287
|
|
|
282
|
-
|
|
288
|
+
def _doctor_apply_fixes(diagnostics: ProjectDiagnostics, *, fix: bool, llm: bool) -> None:
|
|
289
|
+
"""Layers 4-5: Apply auto-fixes and optionally ask LLM for suggestions."""
|
|
283
290
|
if fix and diagnostics.issues:
|
|
284
291
|
console.print("\n[bold]Layer 4: Attempting auto-fix...[/]")
|
|
285
292
|
fixed = diagnostics.auto_fix()
|
|
286
293
|
if fixed > 0:
|
|
287
294
|
console.print(f"[green]✓ Fixed {fixed} issue(s)[/]")
|
|
288
295
|
|
|
289
|
-
# Layer 5: LLM assist
|
|
290
296
|
if llm and diagnostics._issues:
|
|
291
297
|
_run_llm_assist(diagnostics)
|
|
292
298
|
|
|
293
|
-
|
|
299
|
+
|
|
300
|
+
def _doctor_print_summary(diagnostics: ProjectDiagnostics) -> None:
|
|
301
|
+
"""Print final doctor summary with next-step guidance."""
|
|
294
302
|
if not diagnostics.issues:
|
|
295
303
|
console.print("\n[bold green]Your project is ready! 🚀[/]")
|
|
296
304
|
console.print("\n[dim]Next steps:[/]")
|
|
@@ -406,6 +414,48 @@ def _collect_remote_envs(config, selected_env: str | None) -> list[tuple] | None
|
|
|
406
414
|
return envs
|
|
407
415
|
|
|
408
416
|
|
|
417
|
+
def _run_fixop_checks(env) -> None:
|
|
418
|
+
"""Run extended fixop infrastructure checks on a remote environment.
|
|
419
|
+
|
|
420
|
+
Checks: DNS, container DNS, UFW forward policy, systemd-resolved, NAT masquerade.
|
|
421
|
+
Degrades gracefully when fixop is not installed.
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
import fixop
|
|
425
|
+
except ImportError:
|
|
426
|
+
return # fixop not installed — skip silently
|
|
427
|
+
|
|
428
|
+
ctx = fixop.HostContext(
|
|
429
|
+
host=env.ssh_host,
|
|
430
|
+
user=env.ssh_user or "root",
|
|
431
|
+
port=env.ssh_port or 22,
|
|
432
|
+
key=getattr(env, "ssh_key", None) or "~/.ssh/id_ed25519",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
_CHECKS = [
|
|
436
|
+
("DNS", fixop.check_host_dns),
|
|
437
|
+
("systemd-resolved", fixop.check_systemd_resolved),
|
|
438
|
+
("UFW forward policy", fixop.check_ufw_forward_policy),
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
found = 0
|
|
442
|
+
for label, check_fn in _CHECKS:
|
|
443
|
+
try:
|
|
444
|
+
issues = check_fn(ctx)
|
|
445
|
+
for issue in issues:
|
|
446
|
+
sev = issue.severity.value
|
|
447
|
+
color = "red" if sev in ("error", "critical") else "yellow"
|
|
448
|
+
console.print(f"[{color}] ⚠ [{label}] {issue.message}[/{color}]")
|
|
449
|
+
if issue.fix_command:
|
|
450
|
+
console.print(f"[dim] Fix: {issue.fix_command}[/]")
|
|
451
|
+
found += 1
|
|
452
|
+
except Exception:
|
|
453
|
+
pass # individual check failure — skip
|
|
454
|
+
|
|
455
|
+
if found == 0:
|
|
456
|
+
console.print("[green] ✓ Infrastructure checks passed (DNS, UFW, systemd-resolved)[/]")
|
|
457
|
+
|
|
458
|
+
|
|
409
459
|
def _diagnose_single_remote_env(env_name, env, taskfile_dir) -> None:
|
|
410
460
|
"""Run all remote diagnostics for a single environment."""
|
|
411
461
|
from taskfile.deploy_utils import test_ssh_connection
|
|
@@ -432,6 +482,9 @@ def _diagnose_single_remote_env(env_name, env, taskfile_dir) -> None:
|
|
|
432
482
|
except Exception as e:
|
|
433
483
|
console.print(f"[yellow] Remote diagnostics error: {e}[/]")
|
|
434
484
|
|
|
485
|
+
# Extended infrastructure checks via fixop (DNS, UFW, TLS, systemd-resolved, NAT)
|
|
486
|
+
_run_fixop_checks(env)
|
|
487
|
+
|
|
435
488
|
|
|
436
489
|
def _run_remote_diagnostics(diagnostics: ProjectDiagnostics) -> None:
|
|
437
490
|
"""Extended remote diagnostics — check containers, images, system status on remote server."""
|
|
@@ -328,70 +328,14 @@ taskfile run deploy --teach
|
|
|
328
328
|
```
|
|
329
329
|
"""
|
|
330
330
|
opts = ctx.obj
|
|
331
|
-
env_group = opts.get("env_group")
|
|
332
331
|
tag_filter = [t.strip() for t in run_tags.split(",")] if run_tags else None
|
|
333
332
|
|
|
334
333
|
try:
|
|
335
|
-
# --explain / --teach mode: analyze without running
|
|
336
334
|
if explain or teach:
|
|
337
|
-
|
|
338
|
-
from taskfile.runner.explainer import (
|
|
339
|
-
TaskExplainer, print_explain_report, print_teach_report,
|
|
340
|
-
)
|
|
341
|
-
config = load_taskfile(opts["taskfile_path"])
|
|
342
|
-
resolver = TaskResolver(
|
|
343
|
-
config,
|
|
344
|
-
env_name=opts["env_name"],
|
|
345
|
-
platform_name=opts["platform_name"],
|
|
346
|
-
var_overrides=opts["var"],
|
|
347
|
-
)
|
|
348
|
-
task_list = list(tasks)
|
|
349
|
-
_check_unknown_tasks(task_list, config.tasks)
|
|
350
|
-
if tag_filter:
|
|
351
|
-
task_list = _filter_tasks_by_tags(config, task_list, tag_filter)
|
|
352
|
-
if not task_list:
|
|
353
|
-
console.print(f"[yellow]No tasks match tags: {', '.join(tag_filter)}[/]")
|
|
354
|
-
sys.exit(0)
|
|
355
|
-
|
|
356
|
-
explainer = TaskExplainer(resolver)
|
|
357
|
-
report = explainer.explain(task_list)
|
|
358
|
-
|
|
359
|
-
if teach:
|
|
360
|
-
print_teach_report(report, task_list, resolver.env_name, config)
|
|
361
|
-
else:
|
|
362
|
-
print_explain_report(report, task_list, resolver.env_name)
|
|
363
|
-
|
|
364
|
-
sys.exit(1 if report.has_errors else 0)
|
|
365
|
-
|
|
366
|
-
if env_group:
|
|
367
|
-
success = _run_env_group(
|
|
368
|
-
taskfile_path=opts["taskfile_path"],
|
|
369
|
-
env_group=env_group,
|
|
370
|
-
task_names=list(tasks),
|
|
371
|
-
platform_name=opts["platform_name"],
|
|
372
|
-
var_overrides=opts["var"],
|
|
373
|
-
dry_run=opts["dry_run"],
|
|
374
|
-
verbose=opts["verbose"],
|
|
375
|
-
)
|
|
335
|
+
_run_explain_mode(opts, list(tasks), tag_filter, teach=teach)
|
|
376
336
|
else:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
env_name=opts["env_name"],
|
|
380
|
-
platform_name=opts["platform_name"],
|
|
381
|
-
var_overrides=opts["var"],
|
|
382
|
-
dry_run=opts["dry_run"],
|
|
383
|
-
verbose=opts["verbose"],
|
|
384
|
-
)
|
|
385
|
-
task_list = list(tasks)
|
|
386
|
-
_check_unknown_tasks(task_list, runner.config.tasks)
|
|
387
|
-
|
|
388
|
-
if tag_filter:
|
|
389
|
-
task_list = _filter_tasks_by_tags(runner.config, task_list, tag_filter)
|
|
390
|
-
if not task_list:
|
|
391
|
-
console.print(f"[yellow]No tasks match tags: {', '.join(tag_filter)}[/]")
|
|
392
|
-
sys.exit(0)
|
|
393
|
-
success = runner.run(task_list)
|
|
394
|
-
sys.exit(0 if success else 1)
|
|
337
|
+
success = _run_tasks(opts, list(tasks), tag_filter)
|
|
338
|
+
sys.exit(0 if success else 1)
|
|
395
339
|
except (TaskfileNotFoundError, TaskfileParseError) as e:
|
|
396
340
|
console.print(f"[red]Error:[/] {e}")
|
|
397
341
|
if isinstance(e, TaskfileNotFoundError):
|
|
@@ -399,6 +343,69 @@ taskfile run deploy --teach
|
|
|
399
343
|
sys.exit(1)
|
|
400
344
|
|
|
401
345
|
|
|
346
|
+
def _run_explain_mode(opts: dict, task_list: list[str], tag_filter: list[str] | None, *, teach: bool) -> None:
|
|
347
|
+
"""Handle --explain / --teach mode: analyze execution plan without running."""
|
|
348
|
+
from taskfile.runner.resolver import TaskResolver
|
|
349
|
+
from taskfile.runner.explainer import (
|
|
350
|
+
TaskExplainer, print_explain_report, print_teach_report,
|
|
351
|
+
)
|
|
352
|
+
config = load_taskfile(opts["taskfile_path"])
|
|
353
|
+
resolver = TaskResolver(
|
|
354
|
+
config,
|
|
355
|
+
env_name=opts["env_name"],
|
|
356
|
+
platform_name=opts["platform_name"],
|
|
357
|
+
var_overrides=opts["var"],
|
|
358
|
+
)
|
|
359
|
+
_check_unknown_tasks(task_list, config.tasks)
|
|
360
|
+
if tag_filter:
|
|
361
|
+
task_list = _filter_tasks_by_tags(config, task_list, tag_filter)
|
|
362
|
+
if not task_list:
|
|
363
|
+
console.print(f"[yellow]No tasks match tags: {', '.join(tag_filter)}[/]")
|
|
364
|
+
sys.exit(0)
|
|
365
|
+
|
|
366
|
+
explainer = TaskExplainer(resolver)
|
|
367
|
+
report = explainer.explain(task_list)
|
|
368
|
+
|
|
369
|
+
if teach:
|
|
370
|
+
print_teach_report(report, task_list, resolver.env_name, config)
|
|
371
|
+
else:
|
|
372
|
+
print_explain_report(report, task_list, resolver.env_name)
|
|
373
|
+
|
|
374
|
+
sys.exit(1 if report.has_errors else 0)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _run_tasks(opts: dict, task_list: list[str], tag_filter: list[str] | None) -> bool:
|
|
378
|
+
"""Execute tasks normally, via env group or single runner. Returns success."""
|
|
379
|
+
env_group = opts.get("env_group")
|
|
380
|
+
if env_group:
|
|
381
|
+
return _run_env_group(
|
|
382
|
+
taskfile_path=opts["taskfile_path"],
|
|
383
|
+
env_group=env_group,
|
|
384
|
+
task_names=task_list,
|
|
385
|
+
platform_name=opts["platform_name"],
|
|
386
|
+
var_overrides=opts["var"],
|
|
387
|
+
dry_run=opts["dry_run"],
|
|
388
|
+
verbose=opts["verbose"],
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
runner = TaskfileRunner(
|
|
392
|
+
taskfile_path=opts["taskfile_path"],
|
|
393
|
+
env_name=opts["env_name"],
|
|
394
|
+
platform_name=opts["platform_name"],
|
|
395
|
+
var_overrides=opts["var"],
|
|
396
|
+
dry_run=opts["dry_run"],
|
|
397
|
+
verbose=opts["verbose"],
|
|
398
|
+
)
|
|
399
|
+
_check_unknown_tasks(task_list, runner.config.tasks)
|
|
400
|
+
|
|
401
|
+
if tag_filter:
|
|
402
|
+
task_list = _filter_tasks_by_tags(runner.config, task_list, tag_filter)
|
|
403
|
+
if not task_list:
|
|
404
|
+
console.print(f"[yellow]No tasks match tags: {', '.join(tag_filter)}[/]")
|
|
405
|
+
sys.exit(0)
|
|
406
|
+
return runner.run(task_list)
|
|
407
|
+
|
|
408
|
+
|
|
402
409
|
def _filter_tasks_by_tags(config, task_names: list[str], tags: list[str]) -> list[str]:
|
|
403
410
|
"""Filter task names to only those whose tags overlap with the requested tags."""
|
|
404
411
|
filtered = []
|
|
@@ -74,7 +74,7 @@ def resolve_variables(text: str, variables: dict[str, str]) -> str:
|
|
|
74
74
|
name = groups.get("n") or groups.get("name") or groups.get("simple")
|
|
75
75
|
default = groups.get("default")
|
|
76
76
|
if name and name in variables:
|
|
77
|
-
return variables[name]
|
|
77
|
+
return str(variables[name])
|
|
78
78
|
if default is not None:
|
|
79
79
|
return default
|
|
80
80
|
# Keep unresolved vars as-is (useful for runtime vars)
|
|
@@ -40,13 +40,23 @@ def expand_deploy_recipe(deploy_section: dict[str, Any], variables: dict[str, st
|
|
|
40
40
|
health_check = deploy_section.get("health_check", "/health")
|
|
41
41
|
health_retries = deploy_section.get("health_retries", 5)
|
|
42
42
|
health_delay = deploy_section.get("health_delay", 5)
|
|
43
|
-
rollback_mode = deploy_section.get("rollback", "manual") # auto | manual
|
|
44
43
|
restart_delay = deploy_section.get("restart_delay", 3) # seconds between stop→start
|
|
45
44
|
tag_var = "${TAG}"
|
|
46
45
|
|
|
47
46
|
tasks: dict[str, dict] = {}
|
|
47
|
+
tasks.update(_build_tasks(images, registry, tag_var))
|
|
48
|
+
tasks.update(_push_tasks(images, registry, tag_var))
|
|
49
|
+
tasks["validate-deploy"] = _validate_task()
|
|
50
|
+
tasks["deploy"] = _deploy_task(strategy, images, registry, tag_var, restart_delay)
|
|
51
|
+
tasks.update(_health_tasks(images, health_check, health_retries, health_delay, registry, tag_var))
|
|
52
|
+
tasks.update(_rollback_tasks(images, registry, tag_var, restart_delay))
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
return tasks
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_tasks(images: dict, registry: str, tag_var: str) -> dict[str, dict]:
|
|
58
|
+
"""Generate build-<svc> and build-all tasks."""
|
|
59
|
+
tasks: dict[str, dict] = {}
|
|
50
60
|
for svc_name, dockerfile in images.items():
|
|
51
61
|
image_var = f"{registry}/{svc_name}:{tag_var}"
|
|
52
62
|
tasks[f"build-{svc_name}"] = {
|
|
@@ -55,8 +65,6 @@ def expand_deploy_recipe(deploy_section: dict[str, Any], variables: dict[str, st
|
|
|
55
65
|
"tags": ["ci", "build"],
|
|
56
66
|
"cmds": [f"docker build -t {image_var} -f {dockerfile} ."],
|
|
57
67
|
}
|
|
58
|
-
|
|
59
|
-
# ── build-all ──
|
|
60
68
|
if len(images) > 1:
|
|
61
69
|
tasks["build-all"] = {
|
|
62
70
|
"desc": "Build all images",
|
|
@@ -65,8 +73,12 @@ def expand_deploy_recipe(deploy_section: dict[str, Any], variables: dict[str, st
|
|
|
65
73
|
"tags": ["ci", "build"],
|
|
66
74
|
"cmds": ["echo 'All images built'"],
|
|
67
75
|
}
|
|
76
|
+
return tasks
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
|
|
79
|
+
def _push_tasks(images: dict, registry: str, tag_var: str) -> dict[str, dict]:
|
|
80
|
+
"""Generate push-<svc> and push-all tasks."""
|
|
81
|
+
tasks: dict[str, dict] = {}
|
|
70
82
|
for svc_name in images:
|
|
71
83
|
image_var = f"{registry}/{svc_name}:{tag_var}"
|
|
72
84
|
tasks[f"push-{svc_name}"] = {
|
|
@@ -76,7 +88,6 @@ def expand_deploy_recipe(deploy_section: dict[str, Any], variables: dict[str, st
|
|
|
76
88
|
"tags": ["ci", "push"],
|
|
77
89
|
"cmds": [f"docker push {image_var}"],
|
|
78
90
|
}
|
|
79
|
-
|
|
80
91
|
if len(images) > 1:
|
|
81
92
|
tasks["push-all"] = {
|
|
82
93
|
"desc": "Push all images to registry",
|
|
@@ -86,9 +97,12 @@ def expand_deploy_recipe(deploy_section: dict[str, Any], variables: dict[str, st
|
|
|
86
97
|
"tags": ["ci", "push"],
|
|
87
98
|
"cmds": [f"echo 'All images pushed to {registry}'"],
|
|
88
99
|
}
|
|
100
|
+
return tasks
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
|
|
103
|
+
def _validate_task() -> dict:
|
|
104
|
+
"""Generate the validate-deploy gate task."""
|
|
105
|
+
return {
|
|
92
106
|
"desc": "Validate deploy artifacts — check for unresolved variables and placeholders",
|
|
93
107
|
"tags": ["ci", "validate"],
|
|
94
108
|
"silent": True,
|
|
@@ -103,60 +117,69 @@ def expand_deploy_recipe(deploy_section: dict[str, Any], variables: dict[str, st
|
|
|
103
117
|
],
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
|
|
120
|
+
|
|
121
|
+
def _deploy_task(
|
|
122
|
+
strategy: str, images: dict, registry: str, tag_var: str, restart_delay: int,
|
|
123
|
+
) -> dict:
|
|
124
|
+
"""Generate the deploy task based on strategy. Dispatches to strategy-specific builders."""
|
|
107
125
|
push_dep = "push-all" if len(images) > 1 else f"push-{list(images)[0]}" if images else None
|
|
108
126
|
deploy_deps = ["validate-deploy"]
|
|
109
127
|
if push_dep:
|
|
110
128
|
deploy_deps.insert(0, push_dep)
|
|
111
129
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
elif strategy == "ssh-push":
|
|
117
|
-
tasks["deploy"] = _ssh_push_deploy(deploy_deps, images, registry, tag_var, restart_delay)
|
|
118
|
-
else:
|
|
119
|
-
tasks["deploy"] = _compose_deploy(deploy_deps)
|
|
120
|
-
|
|
121
|
-
# ── Health check ──
|
|
122
|
-
tasks["health"] = {
|
|
123
|
-
"desc": "Check application health",
|
|
124
|
-
"tags": ["ops", "health"],
|
|
125
|
-
"silent": True,
|
|
126
|
-
"retries": health_retries,
|
|
127
|
-
"retry_delay": health_delay,
|
|
128
|
-
"cmds": [
|
|
129
|
-
f"curl -sf https://${{DOMAIN}}{health_check} && echo 'OK' || exit 1",
|
|
130
|
-
],
|
|
130
|
+
_STRATEGY_DISPATCH = {
|
|
131
|
+
"compose": lambda: _compose_deploy(deploy_deps),
|
|
132
|
+
"quadlet": lambda: _quadlet_deploy(deploy_deps, images, registry, tag_var, restart_delay),
|
|
133
|
+
"ssh-push": lambda: _ssh_push_deploy(deploy_deps, images, registry, tag_var, restart_delay),
|
|
131
134
|
}
|
|
135
|
+
builder = _STRATEGY_DISPATCH.get(strategy, _STRATEGY_DISPATCH["compose"])
|
|
136
|
+
return builder()
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
138
|
+
|
|
139
|
+
def _health_tasks(
|
|
140
|
+
images: dict, health_check: str, health_retries: int, health_delay: int,
|
|
141
|
+
registry: str, tag_var: str,
|
|
142
|
+
) -> dict[str, dict]:
|
|
143
|
+
"""Generate health and post-deploy health gate tasks."""
|
|
144
|
+
return {
|
|
145
|
+
"health": {
|
|
146
|
+
"desc": "Check application health",
|
|
147
|
+
"tags": ["ops", "health"],
|
|
148
|
+
"silent": True,
|
|
149
|
+
"retries": health_retries,
|
|
150
|
+
"retry_delay": health_delay,
|
|
151
|
+
"cmds": [
|
|
152
|
+
f"curl -sf https://${{DOMAIN}}{health_check} && echo 'OK' || exit 1",
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
"post-deploy": {
|
|
156
|
+
"desc": "Post-deploy health gate — verify all services are healthy after deploy",
|
|
157
|
+
"tags": ["deploy", "health"],
|
|
158
|
+
"silent": True,
|
|
159
|
+
"retries": health_retries,
|
|
160
|
+
"retry_delay": health_delay,
|
|
161
|
+
"cmds": _post_deploy_health_cmds(images, health_check, registry, tag_var),
|
|
162
|
+
},
|
|
141
163
|
}
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
|
|
166
|
+
def _rollback_tasks(images: dict, registry: str, tag_var: str, restart_delay: int) -> dict[str, dict]:
|
|
167
|
+
"""Generate rollback task if images are defined."""
|
|
168
|
+
if not images:
|
|
169
|
+
return {}
|
|
170
|
+
rollback_cmds = []
|
|
171
|
+
for svc_name in images:
|
|
172
|
+
image_var = f"{registry}/{svc_name}:{tag_var}"
|
|
173
|
+
rollback_cmds.append(f"@remote podman pull {image_var}")
|
|
174
|
+
for svc_name in images:
|
|
175
|
+
rollback_cmds.extend(_graceful_restart_cmds(svc_name, restart_delay))
|
|
176
|
+
return {
|
|
177
|
+
"rollback": {
|
|
154
178
|
"desc": "Rollback to specified version (--var TAG=<prev>)",
|
|
155
179
|
"tags": ["deploy", "rollback"],
|
|
156
180
|
"cmds": rollback_cmds,
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return tasks
|
|
181
|
+
},
|
|
182
|
+
}
|
|
160
183
|
|
|
161
184
|
|
|
162
185
|
def _graceful_restart_cmds(svc_name: str, restart_delay: int = 3) -> list[str]:
|