taskfile 0.3.4__tar.gz → 0.3.6__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 (52) hide show
  1. {taskfile-0.3.4/src/taskfile.egg-info → taskfile-0.3.6}/PKG-INFO +4 -7
  2. {taskfile-0.3.4 → taskfile-0.3.6}/pyproject.toml +3 -1
  3. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/__init__.py +1 -1
  4. taskfile-0.3.6/src/taskfile/cigen/__init__.py +58 -0
  5. taskfile-0.3.6/src/taskfile/cigen/base.py +67 -0
  6. taskfile-0.3.6/src/taskfile/cigen/drone.py +49 -0
  7. taskfile-0.3.6/src/taskfile/cigen/gitea.py +51 -0
  8. taskfile-0.3.6/src/taskfile/cigen/github.py +103 -0
  9. taskfile-0.3.6/src/taskfile/cigen/gitlab.py +83 -0
  10. taskfile-0.3.6/src/taskfile/cigen/jenkins.py +54 -0
  11. taskfile-0.3.6/src/taskfile/cigen/makefile.py +53 -0
  12. taskfile-0.3.6/src/taskfile/cli/__init__.py +6 -0
  13. taskfile-0.3.6/src/taskfile/cli/ci.py +202 -0
  14. taskfile-0.3.6/src/taskfile/cli/deploy.py +170 -0
  15. taskfile-0.3.6/src/taskfile/cli/main.py +197 -0
  16. taskfile-0.3.6/src/taskfile/cli/quadlet.py +189 -0
  17. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/models.py +45 -0
  18. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/parser.py +7 -0
  19. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/quadlet.py +81 -76
  20. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/runner.py +40 -3
  21. taskfile-0.3.6/src/taskfile/scaffold/__init__.py +23 -0
  22. taskfile-0.3.6/src/taskfile/scaffold/codereview.py +207 -0
  23. taskfile-0.3.6/src/taskfile/scaffold/full.py +181 -0
  24. taskfile-0.3.6/src/taskfile/scaffold/minimal.py +40 -0
  25. taskfile-0.3.6/src/taskfile/scaffold/multiplatform.py +222 -0
  26. taskfile-0.3.6/src/taskfile/scaffold/podman.py +94 -0
  27. taskfile-0.3.6/src/taskfile/scaffold/web.py +93 -0
  28. {taskfile-0.3.4 → taskfile-0.3.6/src/taskfile.egg-info}/PKG-INFO +4 -7
  29. taskfile-0.3.6/src/taskfile.egg-info/SOURCES.txt +44 -0
  30. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/requires.txt +2 -0
  31. taskfile-0.3.6/tests/test_cigen.py +301 -0
  32. taskfile-0.3.6/tests/test_cli.py +151 -0
  33. taskfile-0.3.6/tests/test_compose.py +53 -0
  34. taskfile-0.3.6/tests/test_models.py +266 -0
  35. taskfile-0.3.6/tests/test_parser.py +48 -0
  36. taskfile-0.3.6/tests/test_quadlet.py +122 -0
  37. taskfile-0.3.6/tests/test_runner.py +87 -0
  38. taskfile-0.3.6/tests/test_scaffold.py +107 -0
  39. taskfile-0.3.4/setup.py +0 -27
  40. taskfile-0.3.4/src/taskfile/cigen.py +0 -514
  41. taskfile-0.3.4/src/taskfile/cli.py +0 -719
  42. taskfile-0.3.4/src/taskfile/scaffold.py +0 -633
  43. taskfile-0.3.4/src/taskfile.egg-info/SOURCES.txt +0 -21
  44. taskfile-0.3.4/tests/test_taskfile.py +0 -1040
  45. {taskfile-0.3.4 → taskfile-0.3.6}/LICENSE +0 -0
  46. {taskfile-0.3.4 → taskfile-0.3.6}/README.md +0 -0
  47. {taskfile-0.3.4 → taskfile-0.3.6}/setup.cfg +0 -0
  48. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/cirunner.py +0 -0
  49. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/compose.py +0 -0
  50. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/dependency_links.txt +0 -0
  51. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/entry_points.txt +0 -0
  52. {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/top_level.txt +0 -0
@@ -1,9 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskfile
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Universal Taskfile runner with multi-environment deploy support. CI/CD agnostic — run locally or from any pipeline.
5
- Home-page: https://github.com/pyfunc/taskfile
6
- Author: Your Name
7
5
  Author-email: Tom Sapletta <tom@sapletta.com>
8
6
  License-Expression: Apache-2.0
9
7
  Project-URL: Homepage, https://github.com/pyfunc/taskfile
@@ -15,7 +13,7 @@ Classifier: Intended Audience :: Developers
15
13
  Classifier: Programming Language :: Python :: 3
16
14
  Classifier: Topic :: Software Development :: Build Tools
17
15
  Classifier: Topic :: System :: Systems Administration
18
- Requires-Python: >=3.6
16
+ Requires-Python: >=3.9
19
17
  Description-Content-Type: text/markdown
20
18
  License-File: LICENSE
21
19
  Requires-Dist: pyyaml>=6.0
@@ -25,10 +23,9 @@ Provides-Extra: dev
25
23
  Requires-Dist: pytest>=7.0; extra == "dev"
26
24
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
27
25
  Requires-Dist: ruff>=0.4.0; extra == "dev"
28
- Dynamic: author
29
- Dynamic: home-page
26
+ Requires-Dist: build>=1.0; extra == "dev"
27
+ Requires-Dist: twine>=5.0; extra == "dev"
30
28
  Dynamic: license-file
31
- Dynamic: requires-python
32
29
 
33
30
  # taskfile
34
31
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "taskfile"
7
- version = "0.3.4"
7
+ version = "0.3.6"
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"
@@ -31,6 +31,8 @@ dev = [
31
31
  "pytest>=7.0",
32
32
  "pytest-cov>=4.0",
33
33
  "ruff>=0.4.0",
34
+ "build>=1.0",
35
+ "twine>=5.0",
34
36
  ]
35
37
 
36
38
  [project.scripts]
@@ -12,7 +12,7 @@ Features:
12
12
  - @remote SSH command execution
13
13
  """
14
14
 
15
- __version__ = "0.3.4"
15
+ __version__ = "0.3.6"
16
16
  __author__ = "Softreck"
17
17
 
18
18
  from taskfile.runner import TaskfileRunner
@@ -0,0 +1,58 @@
1
+ """CI/CD config generator — generates platform-specific CI/CD files."""
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ from taskfile.models import TaskfileConfig
5
+ from taskfile.cigen.base import TARGETS, CITarget, console
6
+ from taskfile.cigen.github import *
7
+ from taskfile.cigen.gitlab import *
8
+ from taskfile.cigen.gitea import *
9
+ from taskfile.cigen.drone import *
10
+ from taskfile.cigen.jenkins import *
11
+ from taskfile.cigen.makefile import *
12
+
13
+ # ─── Public API ───────────────────────────────────────────
14
+
15
+ def generate_ci(
16
+ config: TaskfileConfig,
17
+ target: str,
18
+ project_dir: str | Path = ".",
19
+ ) -> Path:
20
+ """Generate CI/CD config for a specific target platform."""
21
+ if target not in TARGETS:
22
+ available = ", ".join(sorted(TARGETS.keys()))
23
+ raise ValueError(f"Unknown CI target: '{target}'. Available: {available}")
24
+
25
+ generator = TARGETS[target](config)
26
+ outpath = generator.write(project_dir)
27
+ console.print(f" [green]✓[/] {generator.description}: {outpath}")
28
+ return outpath
29
+
30
+
31
+ def generate_all_ci(
32
+ config: TaskfileConfig,
33
+ project_dir: str | Path = ".",
34
+ targets: list[str] | None = None,
35
+ ) -> list[Path]:
36
+ """Generate CI/CD configs for multiple targets."""
37
+ target_list = targets or list(TARGETS.keys())
38
+ generated = []
39
+ for target in target_list:
40
+ path = generate_ci(config, target, project_dir)
41
+ generated.append(path)
42
+ return generated
43
+
44
+
45
+ def list_targets() -> list[tuple[str, str, str]]:
46
+ """Return list of (name, output_path, description) for all registered targets."""
47
+ result = []
48
+ for name, cls in sorted(TARGETS.items()):
49
+ result.append((name, cls.output_path, cls.description))
50
+ return result
51
+
52
+
53
+ def preview_ci(config: TaskfileConfig, target: str) -> str:
54
+ """Generate CI/CD config content without writing to disk."""
55
+ if target not in TARGETS:
56
+ available = ", ".join(sorted(TARGETS.keys()))
57
+ raise ValueError(f"Unknown CI target: '{target}'. Available: {available}")
58
+ return TARGETS[target](config).generate()
@@ -0,0 +1,67 @@
1
+ """Base definitions for CI/CD generators."""
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ import yaml
5
+ from rich.console import Console
6
+ from taskfile.models import TaskfileConfig, PipelineStage
7
+
8
+ console = Console()
9
+
10
+ TARGETS: dict[str, type["CITarget"]] = {}
11
+
12
+ def register_target(name: str):
13
+ def decorator(cls):
14
+ TARGETS[name] = cls
15
+ return cls
16
+ return decorator
17
+
18
+ class CITarget:
19
+ """Base class for CI/CD target generators."""
20
+
21
+ name: str = ""
22
+ output_path: str = ""
23
+ description: str = ""
24
+
25
+ def __init__(self, config: TaskfileConfig):
26
+ self.config = config
27
+ self.pipeline = config.pipeline
28
+
29
+ def generate(self) -> str:
30
+ """Generate the CI/CD config content. Override in subclasses."""
31
+ raise NotImplementedError
32
+
33
+ def write(self, project_dir: str | Path = ".") -> Path:
34
+ """Write the generated config to the appropriate file."""
35
+ outpath = Path(project_dir) / self.output_path
36
+ outpath.parent.mkdir(parents=True, exist_ok=True)
37
+ content = self.generate()
38
+ outpath.write_text(content)
39
+ return outpath
40
+
41
+ def _tag_var(self) -> str:
42
+ """Platform-specific variable for commit SHA / tag."""
43
+ return "latest"
44
+
45
+ def _stage_env_flag(self, stage: PipelineStage) -> str:
46
+ """Build --env flag for a stage."""
47
+ env = stage.env or self.config.default_env
48
+ return f"--env {env}" if env else ""
49
+
50
+ def _stage_tasks_cmd(self, stage: PipelineStage) -> str:
51
+ """Build the taskfile run command for a stage."""
52
+ tasks = " ".join(stage.tasks)
53
+ env_flag = self._stage_env_flag(stage)
54
+ tag = self._tag_var()
55
+ return f"taskfile {env_flag} run {tasks} --var TAG={tag}".strip()
56
+
57
+ def _sanitize_id(name: str) -> str:
58
+ """Make a name safe for use as YAML key / job ID."""
59
+ return name.replace(" ", "-").replace("/", "-").lower()
60
+
61
+ def _yaml_dump(data: dict) -> str:
62
+ """Dump dict to YAML with a generation header."""
63
+ header = (
64
+ "# Auto-generated from Taskfile.yml — do not edit manually\n"
65
+ "# Regenerate with: taskfile ci generate\n\n"
66
+ )
67
+ return header + yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+ from taskfile.cigen.base import CITarget, register_target, _sanitize_id, _yaml_dump
3
+
4
+ # ─── Drone CI ─────────────────────────────────────────────
5
+
6
+ @register_target("drone")
7
+ class DroneCITarget(CITarget):
8
+ name = "drone"
9
+ output_path = ".drone.yml"
10
+ description = "Drone CI"
11
+
12
+ def _tag_var(self) -> str:
13
+ return "${DRONE_COMMIT_SHA:0:8}"
14
+
15
+ def generate(self) -> str:
16
+ p = self.pipeline
17
+
18
+ doc: dict = {
19
+ "kind": "pipeline",
20
+ "type": "docker",
21
+ "name": self.config.name or "default",
22
+ "trigger": {"branch": p.branches or ["main"]},
23
+ "steps": [],
24
+ }
25
+
26
+ for stage in p.stages:
27
+ step: dict = {
28
+ "name": stage.name,
29
+ "image": f"python:{p.python_version}-slim",
30
+ "commands": [
31
+ p.install_cmd,
32
+ self._stage_tasks_cmd(stage),
33
+ ],
34
+ }
35
+
36
+ if stage.when == "manual":
37
+ step["when"] = {"event": ["promote"]}
38
+
39
+ needs_dind = stage.docker_in_docker or p.docker_in_docker
40
+ if needs_dind:
41
+ step["image"] = "docker"
42
+ step["volumes"] = [{"name": "docker", "path": "/var/run/docker.sock"}]
43
+
44
+ doc["steps"].append(step)
45
+
46
+ if any(s.docker_in_docker or p.docker_in_docker for s in p.stages):
47
+ doc["volumes"] = [{"name": "docker", "host": {"path": "/var/run/docker.sock"}}]
48
+
49
+ return _yaml_dump(doc)
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+ from taskfile.cigen.base import CITarget, register_target, _sanitize_id, _yaml_dump
3
+
4
+ # ─── Gitea Actions ────────────────────────────────────────
5
+
6
+ @register_target("gitea")
7
+ class GiteaActionsTarget(CITarget):
8
+ name = "gitea"
9
+ output_path = ".gitea/workflows/taskfile.yml"
10
+ description = "Gitea Actions"
11
+
12
+ def _tag_var(self) -> str:
13
+ return "${{ github.sha }}" # Gitea uses same syntax as GitHub
14
+
15
+ def generate(self) -> str:
16
+ """Gitea Actions are mostly GitHub Actions compatible."""
17
+ p = self.pipeline
18
+ branches = p.branches or ["main"]
19
+
20
+ workflow: dict = {
21
+ "name": self.config.name or "Taskfile Pipeline",
22
+ "on": {
23
+ "push": {"branches": branches},
24
+ },
25
+ "jobs": {},
26
+ }
27
+
28
+ prev_job: str | None = None
29
+
30
+ for stage in p.stages:
31
+ job_id = _sanitize_id(stage.name)
32
+ runner = stage.runner or p.runner_image or "ubuntu-latest"
33
+
34
+ steps: list[dict] = [
35
+ {"uses": "actions/checkout@v4"},
36
+ {"run": f"pip install --break-system-packages taskfile || {p.install_cmd}"},
37
+ {"name": f"Run: {stage.name}", "run": self._stage_tasks_cmd(stage)},
38
+ ]
39
+
40
+ job: dict = {"runs-on": runner, "steps": steps}
41
+ if prev_job:
42
+ job["needs"] = prev_job
43
+
44
+ if stage.when == "manual":
45
+ # Gitea doesn't fully support workflow_dispatch, skip manual stages
46
+ job["if"] = "false # manual trigger not supported in Gitea"
47
+
48
+ workflow["jobs"][job_id] = job
49
+ prev_job = job_id
50
+
51
+ return _yaml_dump(workflow)
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+ from taskfile.cigen.base import CITarget, register_target, _sanitize_id, _yaml_dump
3
+
4
+ # ─── GitHub Actions ───────────────────────────────────────
5
+
6
+ @register_target("github")
7
+ class GitHubActionsTarget(CITarget):
8
+ name = "github"
9
+ output_path = ".github/workflows/taskfile.yml"
10
+ description = "GitHub Actions"
11
+
12
+ def _tag_var(self) -> str:
13
+ return "${{ github.sha }}"
14
+
15
+ def _build_steps(self, stage) -> list[dict]:
16
+ p = self.pipeline
17
+ needs_dind = stage.docker_in_docker or p.docker_in_docker
18
+ steps: list[dict] = [
19
+ {"uses": "actions/checkout@v4"},
20
+ {"uses": "actions/setup-python@v5", "with": {"python-version": p.python_version}},
21
+ {"run": p.install_cmd},
22
+ ]
23
+
24
+ if stage.env and stage.env != "local":
25
+ steps.append({
26
+ "name": "Setup SSH key",
27
+ "run": (
28
+ "mkdir -p ~/.ssh\n"
29
+ 'echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519\n'
30
+ "chmod 600 ~/.ssh/id_ed25519"
31
+ ),
32
+ })
33
+
34
+ if needs_dind:
35
+ steps.append({
36
+ "name": "Login to Container Registry",
37
+ "run": (
38
+ 'echo "${{ secrets.REGISTRY_TOKEN }}" | '
39
+ "docker login ghcr.io -u ${{ github.actor }} --password-stdin"
40
+ ),
41
+ })
42
+
43
+ steps.append({
44
+ "name": f"Run: {stage.name}",
45
+ "run": self._stage_tasks_cmd(stage),
46
+ })
47
+
48
+ arts = stage.artifacts or p.artifacts
49
+ if arts:
50
+ steps.append({
51
+ "uses": "actions/upload-artifact@v4",
52
+ "with": {
53
+ "name": f"{stage.name}-artifacts",
54
+ "path": "\n".join(arts),
55
+ },
56
+ })
57
+
58
+ return steps
59
+
60
+ def _apply_conditions(self, job: dict, stage) -> None:
61
+ if stage.when == "manual":
62
+ job["if"] = "github.event_name == 'workflow_dispatch'"
63
+ elif stage.when.startswith("branch:"):
64
+ branch = stage.when.split(":", 1)[1]
65
+ job["if"] = f"github.ref == 'refs/heads/{branch}'"
66
+ elif stage.when == "tag":
67
+ job["if"] = "startsWith(github.ref, 'refs/tags/')"
68
+
69
+ def generate(self) -> str:
70
+ p = self.pipeline
71
+ branches = p.branches or ["main"]
72
+
73
+ workflow: dict = {
74
+ "name": self.config.name or "Taskfile Pipeline",
75
+ "on": {
76
+ "push": {"branches": branches},
77
+ "workflow_dispatch": {},
78
+ },
79
+ "jobs": {},
80
+ }
81
+
82
+ prev_job: str | None = None
83
+
84
+ for stage in p.stages:
85
+ job_id = _sanitize_id(stage.name)
86
+ runner = stage.runner or p.runner_image or "ubuntu-latest"
87
+
88
+ steps = self._build_steps(stage)
89
+
90
+ job: dict = {
91
+ "runs-on": runner,
92
+ "steps": steps,
93
+ }
94
+
95
+ if prev_job:
96
+ job["needs"] = prev_job
97
+
98
+ self._apply_conditions(job, stage)
99
+
100
+ workflow["jobs"][job_id] = job
101
+ prev_job = job_id
102
+
103
+ return _yaml_dump(workflow)
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+ from taskfile.cigen.base import CITarget, register_target, _sanitize_id, _yaml_dump
3
+
4
+ # ─── GitLab CI ────────────────────────────────────────────
5
+
6
+ @register_target("gitlab")
7
+ class GitLabCITarget(CITarget):
8
+ name = "gitlab"
9
+ output_path = ".gitlab-ci.yml"
10
+ description = "GitLab CI"
11
+
12
+ def _tag_var(self) -> str:
13
+ return "$CI_COMMIT_SHORT_SHA"
14
+
15
+ def generate(self) -> str:
16
+ p = self.pipeline
17
+
18
+ doc: dict = {
19
+ "stages": [s.name for s in p.stages],
20
+ }
21
+
22
+ # Default image
23
+ image = f"python:{p.python_version}-slim"
24
+ doc["default"] = {"image": image}
25
+
26
+ # Cache
27
+ caches = p.cache
28
+ if caches:
29
+ doc["default"]["cache"] = {
30
+ "key": "$CI_COMMIT_REF_SLUG",
31
+ "paths": caches,
32
+ }
33
+
34
+ for stage in p.stages:
35
+ job_id = _sanitize_id(stage.name)
36
+ job: dict = {
37
+ "stage": stage.name,
38
+ "script": [
39
+ p.install_cmd,
40
+ self._stage_tasks_cmd(stage),
41
+ ],
42
+ }
43
+
44
+ needs_dind = stage.docker_in_docker or p.docker_in_docker
45
+ if needs_dind:
46
+ job["image"] = "docker:latest"
47
+ job["services"] = ["docker:dind"]
48
+ job["variables"] = {"DOCKER_TLS_CERTDIR": "/certs"}
49
+ job["before_script"] = [
50
+ "apk add --no-cache python3 py3-pip",
51
+ p.install_cmd,
52
+ ]
53
+ # Remove install from main script since it's in before_script
54
+ job["script"] = [self._stage_tasks_cmd(stage)]
55
+
56
+ if stage.when == "manual":
57
+ job["when"] = "manual"
58
+ elif stage.when == "tag":
59
+ job["rules"] = [{"if": "$CI_COMMIT_TAG"}]
60
+
61
+ # Restrict to branches
62
+ branches = p.branches
63
+ if branches and stage.when == "auto":
64
+ job["only"] = branches
65
+
66
+ # SSH setup for remote stages
67
+ if stage.env and stage.env != "local":
68
+ before = job.get("before_script", [])
69
+ before.extend([
70
+ 'mkdir -p ~/.ssh',
71
+ 'echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519',
72
+ 'chmod 600 ~/.ssh/id_ed25519',
73
+ ])
74
+ job["before_script"] = before
75
+
76
+ # Artifacts
77
+ arts = stage.artifacts or p.artifacts
78
+ if arts:
79
+ job["artifacts"] = {"paths": arts}
80
+
81
+ doc[job_id] = job
82
+
83
+ return _yaml_dump(doc)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+ from taskfile.cigen.base import CITarget, register_target, _sanitize_id, _yaml_dump
3
+
4
+ # ─── Jenkins ──────────────────────────────────────────────
5
+
6
+ @register_target("jenkins")
7
+ class JenkinsTarget(CITarget):
8
+ name = "jenkins"
9
+ output_path = "Jenkinsfile"
10
+ description = "Jenkins Pipeline"
11
+
12
+ def _tag_var(self) -> str:
13
+ return "${GIT_COMMIT[0..7]}"
14
+
15
+ def generate(self) -> str:
16
+ p = self.pipeline
17
+ lines = [
18
+ "// Auto-generated from Taskfile.yml — do not edit manually",
19
+ "// Regenerate with: taskfile ci generate --target jenkins",
20
+ "pipeline {",
21
+ f" agent {{ docker {{ image 'python:{p.python_version}-slim' }} }}",
22
+ "",
23
+ " environment {",
24
+ f" TAG = \"${{GIT_COMMIT.take(8)}}\"",
25
+ " }",
26
+ "",
27
+ " stages {",
28
+ ]
29
+
30
+ for stage in p.stages:
31
+ env_flag = self._stage_env_flag(stage)
32
+ tasks = " ".join(stage.tasks)
33
+ lines.extend([
34
+ f" stage('{stage.name}') {{",
35
+ " steps {",
36
+ f" sh '{p.install_cmd}'",
37
+ f" sh 'taskfile {env_flag} run {tasks} --var TAG=${{TAG}}'",
38
+ " }",
39
+ " }",
40
+ ])
41
+
42
+ lines.extend([
43
+ " }",
44
+ "",
45
+ " post {",
46
+ " failure {",
47
+ " echo 'Pipeline failed!'",
48
+ " }",
49
+ " }",
50
+ "}",
51
+ "",
52
+ ])
53
+
54
+ return "\n".join(lines)
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+ from taskfile.cigen.base import CITarget, register_target, _sanitize_id, _yaml_dump
3
+
4
+ # ─── Makefile (compatibility wrapper) ─────────────────────
5
+
6
+ @register_target("makefile")
7
+ class MakefileTarget(CITarget):
8
+ name = "makefile"
9
+ output_path = "Makefile"
10
+ description = "GNU Makefile"
11
+
12
+ def generate(self) -> str:
13
+ lines = [
14
+ "# Auto-generated from Taskfile.yml — do not edit manually",
15
+ "# Regenerate with: taskfile ci generate --target makefile",
16
+ "",
17
+ "ENV ?= local",
18
+ "TAG ?= latest",
19
+ "",
20
+ ".PHONY: help",
21
+ "help: ## Show available targets",
22
+ '\t@grep -E \'^[a-zA-Z_-]+:.*?## .*$$\' $(MAKEFILE_LIST) | '
23
+ "sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \" \\033[36m%-20s\\033[0m %s\\n\", $$1, $$2}'",
24
+ "",
25
+ ]
26
+
27
+ # Generate targets for each task
28
+ for task_name, task in sorted(self.config.tasks.items()):
29
+ desc = task.description or task_name
30
+ target = task_name.replace(":", "-")
31
+ lines.append(f".PHONY: {target}")
32
+ lines.append(f"{target}: ## {desc}")
33
+ lines.append(f"\ttaskfile --env $(ENV) run {task_name} --var TAG=$(TAG)")
34
+ lines.append("")
35
+
36
+ # Pipeline stages as composite targets
37
+ if self.pipeline.stages:
38
+ lines.append("# ─── Pipeline stages ─────────────────")
39
+ for stage in self.pipeline.stages:
40
+ tasks_str = " ".join(stage.tasks)
41
+ env_flag = f"--env {stage.env}" if stage.env else "--env $(ENV)"
42
+ lines.append(f".PHONY: stage-{stage.name}")
43
+ lines.append(f"stage-{stage.name}: ## Pipeline stage: {stage.name}")
44
+ lines.append(f"\ttaskfile {env_flag} run {tasks_str} --var TAG=$(TAG)")
45
+ lines.append("")
46
+
47
+ # Full pipeline
48
+ stage_targets = " ".join(f"stage-{s.name}" for s in self.pipeline.stages)
49
+ lines.append(f".PHONY: pipeline")
50
+ lines.append(f"pipeline: {stage_targets} ## Run full pipeline")
51
+ lines.append("")
52
+
53
+ return "\n".join(lines)
@@ -0,0 +1,6 @@
1
+ from taskfile.cli.main import main
2
+ import taskfile.cli.deploy
3
+ import taskfile.cli.quadlet
4
+ import taskfile.cli.ci
5
+
6
+ __all__ = ["main"]