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.
- {taskfile-0.3.4/src/taskfile.egg-info → taskfile-0.3.6}/PKG-INFO +4 -7
- {taskfile-0.3.4 → taskfile-0.3.6}/pyproject.toml +3 -1
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/__init__.py +1 -1
- taskfile-0.3.6/src/taskfile/cigen/__init__.py +58 -0
- taskfile-0.3.6/src/taskfile/cigen/base.py +67 -0
- taskfile-0.3.6/src/taskfile/cigen/drone.py +49 -0
- taskfile-0.3.6/src/taskfile/cigen/gitea.py +51 -0
- taskfile-0.3.6/src/taskfile/cigen/github.py +103 -0
- taskfile-0.3.6/src/taskfile/cigen/gitlab.py +83 -0
- taskfile-0.3.6/src/taskfile/cigen/jenkins.py +54 -0
- taskfile-0.3.6/src/taskfile/cigen/makefile.py +53 -0
- taskfile-0.3.6/src/taskfile/cli/__init__.py +6 -0
- taskfile-0.3.6/src/taskfile/cli/ci.py +202 -0
- taskfile-0.3.6/src/taskfile/cli/deploy.py +170 -0
- taskfile-0.3.6/src/taskfile/cli/main.py +197 -0
- taskfile-0.3.6/src/taskfile/cli/quadlet.py +189 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/models.py +45 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/parser.py +7 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/quadlet.py +81 -76
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/runner.py +40 -3
- taskfile-0.3.6/src/taskfile/scaffold/__init__.py +23 -0
- taskfile-0.3.6/src/taskfile/scaffold/codereview.py +207 -0
- taskfile-0.3.6/src/taskfile/scaffold/full.py +181 -0
- taskfile-0.3.6/src/taskfile/scaffold/minimal.py +40 -0
- taskfile-0.3.6/src/taskfile/scaffold/multiplatform.py +222 -0
- taskfile-0.3.6/src/taskfile/scaffold/podman.py +94 -0
- taskfile-0.3.6/src/taskfile/scaffold/web.py +93 -0
- {taskfile-0.3.4 → taskfile-0.3.6/src/taskfile.egg-info}/PKG-INFO +4 -7
- taskfile-0.3.6/src/taskfile.egg-info/SOURCES.txt +44 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/requires.txt +2 -0
- taskfile-0.3.6/tests/test_cigen.py +301 -0
- taskfile-0.3.6/tests/test_cli.py +151 -0
- taskfile-0.3.6/tests/test_compose.py +53 -0
- taskfile-0.3.6/tests/test_models.py +266 -0
- taskfile-0.3.6/tests/test_parser.py +48 -0
- taskfile-0.3.6/tests/test_quadlet.py +122 -0
- taskfile-0.3.6/tests/test_runner.py +87 -0
- taskfile-0.3.6/tests/test_scaffold.py +107 -0
- taskfile-0.3.4/setup.py +0 -27
- taskfile-0.3.4/src/taskfile/cigen.py +0 -514
- taskfile-0.3.4/src/taskfile/cli.py +0 -719
- taskfile-0.3.4/src/taskfile/scaffold.py +0 -633
- taskfile-0.3.4/src/taskfile.egg-info/SOURCES.txt +0 -21
- taskfile-0.3.4/tests/test_taskfile.py +0 -1040
- {taskfile-0.3.4 → taskfile-0.3.6}/LICENSE +0 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/README.md +0 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/setup.cfg +0 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/cirunner.py +0 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile/compose.py +0 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/dependency_links.txt +0 -0
- {taskfile-0.3.4 → taskfile-0.3.6}/src/taskfile.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
29
|
-
|
|
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.
|
|
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]
|
|
@@ -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)
|