Skill: shenghoo123-png/cicd-templates-generator (version 1.0.1)

# SKILL.md — CI/CD Templates Generator ## Identity **Name**: CI/CD Templates Generator (`cicd-gen`) **Type**: CLI Tool + OpenClaw Skill **Purpose**: 一键生成生产级 CI/CD workflow 文件（GitHub Actions / GitLab CI / Jenkins） **Author**: kay **Version**: 1.0.0 --- ## Triggers ``` cicd-gen <language> [framework] [options] ``` 触发词示例： - "生成 Python Flask CI/CD workflow" - "创建 GitHub Actions workflow for pytest + Docker" - "GitLab CI for Node.js + Jest" - "Generate Jenkinsfile for Go + goreleaser" - "多平台 CI/CD: Python + pytest + Docker" - "帮我创建一个 CI/CD workflow" - "CI/CD 模板生成" --- ## Capabilities ### 支持的语言与框架 | 语言 | 框架 | 测试框架 | 部署选项 | 发布 | |------|------|----------|----------|------| | Python | flask, django, fastapi | pytest, unittest | docker, azure, aliyun, tencent, k8s, serverless | — | | JavaScript | node, express | jest, mocha | docker, azure, aliyun, tencent | npm publish | | Go | gin, echo | go-test | docker | goreleaser | ### 支持的场景 - **定时任务** (cron): 集成于 GitHub Actions `on.schedule` - **PR 测试**: 集成于 `on.pull_request` - **自动发布**: goreleaser / npm publish - **多平台矩阵**: 同时输出 GitHub Actions + GitLab CI + Jenkinsfile - **Docker 构建推送**: docker build + push 到 Docker Hub - **云部署**: Azure Web App / 阿里云 ECS / 腾讯云 / Kubernetes / Serverless --- ## CLI Usage ```bash # 基本用法 cicd-gen python flask --test pytest --output ./.github/workflows/ # Python + pytest + Docker cicd-gen python flask --test pytest --deploy docker # Python + pytest + Docker + Azure 部署 cicd-gen python flask --test pytest --deploy docker --cloud azure # Python + unittest + 覆盖率 cicd-gen python django --test unittest --coverage # JavaScript + Jest cicd-gen javascript node --test jest # JavaScript + Jest + Docker + npm 发布 cicd-gen javascript node --test jest --deploy docker --release npm # Go + goreleaser cicd-gen go gin --test go-test --release goreleaser # 多平台输出 cicd-gen python flask --test pytest --format all # 仅打印不写文件 cicd-gen python flask --test pytest --print-only # 自定义版本 cicd-gen python flask --test pytest --python-version 3.10 --name "My CI" ``` ### 参数说明 | 参数 | 说明 | 默认值 | |------|------|--------| | `language` | python / javascript / go | 必填 | | `framework` | flask / django / node / gin / echo 等 | 可选 | | `--test, -t` | 测试框架 | 自动检测 (pytest/jest/go-test) | | `--deploy, -d` | 部署目标 | 无 | | `--release, -r` | 发布工具 (npm/goreleaser) | 无 | | `--coverage, -c` | 启用覆盖率报告 | False | | `--cloud` | 云服务商 (azure/aliyun/tencent) | 无 | | `--format, -f` | github / gitlab / jenkins / all | github | | `--output, -o` | 输出目录 | ./.github/workflows | | `--name, -n` | workflow 名称 | CI | | `--python-version` | Python 版本 | 3.11 | | `--node-version` | Node 版本 | 20 | | `--go-version` | Go 版本 | 1.22 | | `--print-only` | 仅打印，不写文件 | False | --- ## Installation ```bash pip install cicd-gen # 或本地安装 cd cicd-templates-generator && pip install -e . ``` --- ## Technical Details - **语言**: Python 3.8+ - **依赖**: `pyyaml` - **CLI 入口**: `cicd-gen` - **代码行数**: 单文件 ≤ 500 行（generator 模块各自独立） - **测试**: 25 个单元测试，全部通过 --- ## 输出示例 ### GitHub Actions (`.github/workflows/ci.yml`) ```yaml name: CI on: push: branches: [main, master] pull_request: branches: [main, master] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: pip install -r requirements.txt pytest - name: Run tests run: pytest - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ secrets.DOCKERHUB_USERNAME }}/app:latest ``` --- ## 文件结构 ``` cicd-templates-generator/ ├── cicd_gen/ │ ├── __init__.py │ ├── cli.py # CLI 入口 │ └── generator/ │ ├── __init__.py # BaseGenerator │ ├── github_actions.py # GitHub Actions 生成器 │ ├── gitlab_ci.py # GitLab CI 生成器 │ └── jenkins.py # Jenkinsfile 生成器 ├── tests/ │ └── test_cicd_gen.py # 单元测试 ├── setup.py ├── SKILL.md └── DESIGN-cicd-templates-generator.md ``` --- ## Integration with OpenClaw 当用户请求创建 CI/CD workflow 时： 1. 解析用户意图（语言、框架、需求） 2. 调用 `cicd-gen` CLI 生成对应 workflow 3. 展示生成结果（`--print-only` 模式） 4. 可选：写入项目目录 ```bash # 示例调用 cicd-gen python flask --test pytest --deploy docker --print-only ```

path: DESIGN-cicd-templates-generator.md
content: # DESIGN - CI/CD Templates Generator

## 1. Concept & Vision

**cicd-gen** — 一键生成生产级 CI/CD Workflow 的 CLI 工具。用户只需提供「语言 + 框架 + 需求」，即可输出可直接提交的 `.github/workflows/ci.yml`（以及 GitLab CI / Jenkinsfile 变体）。

定位：面向独立开发者/小团队，让 CI/CD 配置从「门槛」变成「随手可得」的基础设施。

---

## 2. Design Language

- **输出风格**：YAML 纯净、无多余注释、可直接提交
- **CLI 交互**： positional args + flag，零配置即可跑
- **错误处理**：用户输入不合法时给出明确的修复建议

---

## 3. Architecture

```
cicd-gen
├── generator/
│   ├── __init__.py
│   ├── base.py          # TemplateEngine 基类
│   ├── github_actions.py # GitHub Actions 生成器
│   ├── gitlab_ci.py      # GitLab CI 生成器
│   └── jenkins.py         # Jenkinsfile 生成器
├── templates/
│   ├── python/          # pytest, unittest, coverage, docker, azure, aliyun, tencent
│   ├── javascript/      # jest, mocha, npm publish, docker
│   └── go/              # go test, goreleaser
├── cli.py               # CLI 入口 (cicd-gen)
└── tests/
    └── test_generator.py
```

---

## 4. Core Features

### 4.1 输入格式
```bash
cicd-gen python flask --test pytest --deploy docker --output .github/workflows/
cicd-gen javascript node --test jest --deploy docker --publish npm
cicd-gen go --test go-test --release goreleaser
```

### 4.2 支持场景矩阵

| 语言 | 测试 | 覆盖率 | Docker | 部署 | 发布 |
|------|------|--------|--------|------|------|
| Python | pytest/unittest | coverage | build+push | azure/aliyun/tencent/k8s/serverless | - |
| JavaScript | jest/mocha | - | build+push | azure/aliyun | npm publish |
| Go | go test | - | build | - | goreleaser |

### 4.3 输出格式
- `--format github` (默认): `.github/workflows/ci.yml`
- `--format gitlab`: `.gitlab-ci.yml`
- `--format jenkins`: `Jenkinsfile`
- `--format all`: 同时输出三种格式

---

## 5. CLI Interface

```bash
cicd-gen <language> [framework] [options]

Arguments:
  language              python | javascript | go
  framework             flask | django | fastapi | node | express | gin | echo

Options:
  --test TEST            pytest | unittest | jest | mocha | go-test
  --deploy DEPLOY        docker | azure | aliyun | tencent | k8s | serverless
  --release RELEASE      goreleaser | npm
  --format FORMAT        github | gitlab | jenkins | all
  --output DIR           输出目录 (默认: ./.github/workflows)
  --name NAME            workflow 名字 (默认: ci)
  --python-version VER   Python 版本 (默认: "3.11")
  --node-version VER     Node 版本 (默认: "20")
  --go-version VER       Go 版本 (默认: "1.22")
```

---

## 6. TDD Test Cases

### 单元测试覆盖
- `test_python_pytest_docker()`: Python + pytest + Docker
- `test_python_unittest_coverage()`: Python + unittest + coverage
- `test_javascript_jest()`: JavaScript + Jest
- `test_go_goreleaser()`: Go + goreleaser
- `test_github_actions_yaml_valid()`: 生成的 YAML 可解析
- `test_gitlab_ci_yaml_valid()`: GitLab CI YAML 可解析
- `test_jenkinsfile_valid()`: Jenkinsfile Groovy 语法合法
- `test_output_directory_creation()`: 自动创建输出目录
- `test_invalid_language()`: 无效语言报错
- `test_combined_features()`: 多特性组合

---

## 7. Quality Rules

- 单文件 ≤ 500 行
- 所有模板输出通过 `yaml.safe_load` 验证
- Jenkinsfile 通过 `jenkins-pipeline-linter` 或 basic Groovy 语法检查（无 raise）
- CLI 退出码：成功 0，参数错误 1，生成失败 2


path: cicd_gen/__init__.py
content: """CI/CD Templates Generator - Generate production-ready CI/CD workflows."""

__version__ = "1.0.0"
__author__ = "kay"

from .generator.github_actions import GitHubActionsGenerator
from .generator.gitlab_ci import GitLabCIGenerator
from .generator.jenkins import JenkinsGenerator

__all__ = ["GitHubActionsGenerator", "GitLabCIGenerator", "JenkinsGenerator"]


path: cicd_gen/cli.py
content: #!/usr/bin/env python3
"""CLI entry point for cicd-gen."""

import argparse
import sys
import os
from pathlib import Path

from cicd_gen.generator.github_actions import GitHubActionsGenerator
from cicd_gen.generator.gitlab_ci import GitLabCIGenerator
from cicd_gen.generator.jenkins import JenkinsGenerator


def parse_args():
    parser = argparse.ArgumentParser(
        prog="cicd-gen",
        description="Generate production-ready CI/CD workflows "
                    "(GitHub Actions, GitLab CI, Jenkins)",
    )
    parser.add_argument(
        "language",
        choices=["python", "javascript", "go"],
        help="Programming language",
    )
    parser.add_argument(
        "framework",
        nargs="?",
        help="Framework (e.g., flask, django, node, gin, echo)",
    )
    parser.add_argument(
        "--test", "-t",
        help="Test framework: pytest, unittest (Python), "
             "jest, mocha (JavaScript), go-test (Go)",
    )
    parser.add_argument(
        "--deploy", "-d",
        help="Deployment target: docker, azure, aliyun, tencent, k8s, serverless",
    )
    parser.add_argument(
        "--release", "-r",
        help="Release tool: npm (JS), goreleaser (Go)",
    )
    parser.add_argument(
        "--coverage", "-c",
        action="store_true",
        help="Enable coverage reporting",
    )
    parser.add_argument(
        "--cloud",
        help="Cloud provider (for --deploy docker + cloud config)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["github", "gitlab", "jenkins", "all"],
        default="github",
        help="Output format (default: github)",
    )
    parser.add_argument(
        "--output", "-o",
        default="./.github/workflows",
        help="Output directory (default: ./.github/workflows)",
    )
    parser.add_argument(
        "--name", "-n",
        default="CI",
        help="Workflow name (default: CI)",
    )
    parser.add_argument(
        "--python-version",
        default="3.11",
        help="Python version (default: 3.11)",
    )
    parser.add_argument(
        "--node-version",
        default="20",
        help="Node.js version (default: 20)",
    )
    parser.add_argument(
        "--go-version",
        default="1.22",
        help="Go version (default: 1.22)",
    )
    parser.add_argument(
        "--print-only",
        action="store_true",
        help="Print to stdout instead of writing files",
    )
    return parser.parse_args()


def get_generators():
    return {
        "github": GitHubActionsGenerator(),
        "gitlab": GitLabCIGenerator(),
        "jenkins": JenkinsGenerator(),
    }


def get_output_info(fmt, output_dir, language):
    if fmt == "github":
        return output_dir, "ci.yml"
    elif fmt == "gitlab":
        return "./", ".gitlab-ci.yml"
    elif fmt == "jenkins":
        return "./", "Jenkinsfile"


def main():
    args = parse_args()
    generators = get_generators()

    # Auto-detect test framework from language if not provided
    test = args.test
    if not test:
        if args.language == "python":
            test = "pytest"
        elif args.language == "javascript":
            test = "jest"
        elif args.language == "go":
            test = "go-test"

    kwargs = dict(
        language=args.language,
        framework=args.framework,
        test=test,
        deploy=args.deploy,
        release=args.release,
        coverage=args.coverage,
        cloud=args.cloud,
        name=args.name,
        python_version=args.python_version,
        node_version=args.node_version,
        go_version=args.go_version,
    )

    formats = ["github", "gitlab", "jenkins"] if args.format == "all" \
              else [args.format]

    exit_code = 0

    for fmt in formats:
        gen = generators[fmt]
        try:
            content = gen.generate(**kwargs)
        except ValueError as e:
            print(f"[cicd-gen] Error: {e}", file=sys.stderr)
            exit_code = 1
            continue

        out_dir, filename = get_output_info(fmt, args.output, args.language)

        if args.print_only:
            print(f"=== {fmt.upper()} ({filename}) ===")
            print(content)
            continue

        try:
            saved_path = gen.save(content, out_dir, filename)
            print(f"[cicd-gen] Generated: {saved_path}")
        except Exception as e:
            print(f"[cicd-gen] Save error: {e}", file=sys.stderr)
            exit_code = 2

    sys.exit(exit_code)


if __name__ == "__main__":
    main()


path: cicd_gen/generator/__init__.py
content: """Base generator class for CI/CD templates."""

import os
from abc import ABC, abstractmethod
from pathlib import Path


class BaseGenerator(ABC):
    """Abstract base class for CI/CD workflow generators."""

    LANGUAGES = {"python", "javascript", "go"}
    PYTHON_TEST_FRAMEWORKS = {"pytest", "unittest"}
    JS_TEST_FRAMEWORKS = {"jest", "mocha"}
    GO_TEST_FRAMEWORKS = {"go-test"}
    DEPLOY_OPTIONS = {"docker", "azure", "aliyun", "tencent", "k8s", "serverless"}
    RELEASE_OPTIONS = {"npm", "goreleaser"}

    def __init__(self):
        self.errors = []

    def validate(self, language, framework=None, test=None, deploy=None,
                 release=None, coverage=None, cloud=None):
        """Validate input parameters."""
        if language not in self.LANGUAGES:
            raise ValueError(
                f"Unsupported language: {language}. "
                f"Supported: {', '.join(sorted(self.LANGUAGES))}"
            )

        if test:
            valid_tests = {
                "python": self.PYTHON_TEST_FRAMEWORKS,
                "javascript": self.JS_TEST_FRAMEWORKS,
                "go": self.GO_TEST_FRAMEWORKS,
            }.get(language, set())
            if test not in valid_tests:
                raise ValueError(
                    f"Unsupported test framework '{test}' for {language}. "
                    f"Supported: {', '.join(sorted(valid_tests))}"
                )

        if deploy and deploy not in self.DEPLOY_OPTIONS:
            raise ValueError(
                f"Unsupported deploy option: {deploy}. "
                f"Supported: {', '.join(sorted(self.DEPLOY_OPTIONS))}"
            )

        if release and release not in self.RELEASE_OPTIONS:
            raise ValueError(
                f"Unsupported release option: {release}. "
                f"Supported: {', '.join(sorted(self.RELEASE_OPTIONS))}"
            )

    def save(self, yaml_str, output_dir, filename="ci.yml"):
        """Save generated content to a file."""
        out_path = Path(output_dir)
        out_path.mkdir(parents=True, exist_ok=True)
        file_path = out_path / filename
        file_path.write_text(yaml_str, encoding="utf-8")
        return str(file_path)

    @abstractmethod
    def generate(self, language, framework=None, test=None, deploy=None,
                 release=None, coverage=None, cloud=None, name="CI",
                 python_version="3.11", node_version="20", go_version="1.22",
                 **kwargs):
        """Generate CI/CD workflow content. Must be implemented by subclasses."""
        pass


path: cicd_gen/generator/github_actions.py
content: """GitHub Actions workflow generator."""

import yaml
from . import BaseGenerator


class GitHubActionsGenerator(BaseGenerator):
    """Generator for GitHub Actions workflows."""

    def generate(self, language, framework=None, test=None, deploy=None,
                 release=None, coverage=None, cloud=None, name="CI",
                 python_version="3.11", node_version="20", go_version="1.22",
                 **kwargs):
        self.validate(language, framework, test, deploy, release)

        if language == "python":
            return self._generate_python(
                framework, test, deploy, coverage, cloud, name, python_version
            )
        elif language == "javascript":
            return self._generate_javascript(
                framework, test, deploy, release, name, node_version
            )
        elif language == "go":
            return self._generate_go(
                framework, test, deploy, release, name, go_version
            )

    def _generate_python(self, framework, test, deploy, coverage, cloud,
                         name, python_version):
        workflow = {
            "name": name,
            "on": {"push": {"branches": ["main", "master"]},
                   "pull_request": {"branches": ["main", "master"]}},
        }

        jobs = {}

        # Pure Docker deploy: add Docker build+push to test job
        if deploy == "docker" and cloud is None:
            test_job = self._build_python_test_job(
                framework, test, coverage, deploy, cloud, python_version
            )
            docker_steps = self._docker_build_push_steps()
            test_job["steps"].extend(docker_steps)
            jobs["test"] = test_job
        else:
            test_job = self._build_python_test_job(
                framework, test, coverage, deploy, cloud, python_version
            )
            jobs["test"] = test_job

            if deploy and deploy != "docker":
                deploy_job = self._build_python_deploy_job(
                    framework, deploy, cloud, python_version
                )
                jobs["deploy"] = deploy_job
                test_job["needs"] = ["deploy"]
            elif deploy == "docker" and cloud:
                # Docker + cloud: create a combined deploy job
                deploy_job = self._build_docker_plus_cloud_job(
                    cloud, python_version
                )
                jobs["deploy"] = deploy_job
                test_job["needs"] = ["deploy"]

        workflow["jobs"] = jobs
        return yaml.dump(workflow, default_flow_style=False, sort_keys=False,
                         allow_unicode=True)

    def _build_python_test_job(self, framework, test, coverage, deploy, cloud,
                                python_version):
        steps = [
            {"name": "Checkout code", "uses": "actions/checkout@v4"},
            {
                "name": f"Set up Python {python_version}",
                "uses": "actions/setup-python@v5",
                "with": {"python-version": python_version},
            },
            {"name": "Install dependencies",
             "run": "pip install -r requirements.txt" +
                    (" pytest pytest-cov" if test == "pytest" and coverage else
                     " pytest" if test == "pytest" else "")},
        ]

        if test == "pytest":
            cov_cmd = "pytest --cov=. --cov-report=xml" if coverage else "pytest"
            steps.append({"name": "Run tests", "run": cov_cmd})
        elif test == "unittest":
            cov_flag = " --coverage" if coverage else ""
            steps.append({
                "name": "Run unittest",
                "run": f"python -m unittest discover -v{cov_flag}"
            })

        if coverage:
            steps.append({
                "name": "Upload coverage",
                "uses": "codecov/codecov-action@v4",
                "with": {"token": "${{ secrets.CODECOV_TOKEN }}"},
            })

        return {
            "runs-on": "ubuntu-latest",
            "steps": steps,
        }

    def _docker_build_push_steps(self):
        return [
            {
                "name": "Log in to Docker Hub",
                "uses": "docker/login-action@v3",
                "with": {
                    "username": "${{ secrets.DOCKERHUB_USERNAME }}",
                    "password": "${{ secrets.DOCKERHUB_TOKEN }}",
                },
            },
            {
                "name": "Build and push Docker image",
                "uses": "docker/build-push-action@v5",
                "with": {
                    "context": ".",
                    "push": True,
                    "tags": "${{ secrets.DOCKERHUB_USERNAME }}/app:latest",
                },
            },
        ]

    def _build_python_deploy_job(self, framework, deploy, cloud, python_version):
        steps = [
            {"name": "Checkout code", "uses": "actions/checkout@v4"},
            {
                "name": f"Set up Python {python_version}",
                "uses": "actions/setup-python@v5",
                "with": {"python-version": python_version},
            },
            {"name": "Install dependencies",
             "run": "pip install -r requirements.txt"},
        ]

        if deploy == "azure":
            steps.extend([
                {"name": "Login to Azure",
                 "uses": "azure/login@v2",
                 "with": {"creds": "${{ secrets.AZURE_CREDENTIALS }}"}},
                {"name": "Deploy to Azure Web App",
                 "uses": "azure/webapps-deploy@v3",
                 "with": {
                     "app-name": "${{ secrets.AZURE_WEBAPP_NAME }}",
                     "slot-name": "production",
                     "publish-profile": "${{ secrets.AZURE_PUBLISH_PROFILE }}",
                 }},
            ])
        elif deploy == "aliyun":
            steps.extend([
                {"name": "Setup Aliyun CLI",
                 "uses": "montagestudio/aliyun-cli@v3",
                 "with": {"access-key-id": "${{ secrets.ALIYUN_ACCESS_KEY_ID }}",
                          "access-key-secret": "${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}",
                          "region": "cn-beijing"}},
                {"name": "Deploy to Aliyun ECS",
                 "run": "aliyun ecs DeployInstance --instance-id ${{ secrets.ALIYUN_INSTANCE_ID }}"},
            ])
        elif deploy == "tencent":
            steps.extend([
                {"name": "Setup Tencent Cloud CLI",
                 "run": "pip install tccli"},
                {"name": "Deploy to Tencent Cloud",
                 "run": "tccli cls deploy --region ap-guangzhou"},
            ])
        elif deploy == "k8s":
            steps.extend([
                {"name": "Configure kubectl",
                 "uses": "azure/k8s-set-context@v3",
                 "with": {"kubeconfig": "${{ secrets.KUBE_CONFIG }}"}},
                {"name": "Deploy to Kubernetes",
                 "uses": "azure/k8s-deploy@v5",
                 "with": {"manifests": "k8s/", "namespace": "default"}},
            ])
        elif deploy == "serverless":
            steps.extend([
                {"name": "Install serverless framework",
                 "run": "npm install -g serverless"},
                {"name": "Deploy serverless app",
                 "run": "serverless deploy",
                 "env": {"AWS_ACCESS_KEY_ID": "${{ secrets.AWS_ACCESS_KEY_ID }}",
                         "AWS_SECRET_ACCESS_KEY": "${{ secrets.AWS_SECRET_ACCESS_KEY }}"}},
            ])

        return {"runs-on": "ubuntu-latest", "steps": steps}

    def _build_docker_plus_cloud_job(self, cloud, python_version):
        steps = [
            {"name": "Checkout code", "uses": "actions/checkout@v4"},
            {
                "name": f"Set up Python {python_version}",
                "uses": "actions/setup-python@v5",
                "with": {"python-version": python_version},
            },
            {"name": "Install dependencies",
             "run": "pip install -r requirements.txt"},
        ]

        # Docker build + push
        steps.extend([
            {
                "name": "Log in to Docker Hub",
                "uses": "docker/login-action@v3",
                "with": {
                    "username": "${{ secrets.DOCKERHUB_USERNAME }}",
                    "password": "${{ secrets.DOCKERHUB_TOKEN }}",
                },
            },
            {
                "name": "Build and push Docker image",
                "uses": "docker/build-push-action@v5",
                "with": {
                    "context": ".",
                    "push": True,
                    "tags": "${{ secrets.DOCKERHUB_USERNAME }}/app:latest",
                },
            },
        ])

        # Cloud-specific deployment steps
        if cloud == "azure":
            steps.extend([
                {"name": "Login to Azure",
                 "uses": "azure/login@v2",
                 "with": {"creds": "${{ secrets.AZURE_CREDENTIALS }}"}},
                {"name": "Deploy to Azure Web App",
                 "uses": "azure/webapps-deploy@v3",
                 "with": {
                     "app-name": "${{ secrets.AZURE_WEBAPP_NAME }}",
                     "slot-name": "production",
                     "publish-profile": "${{ secrets.AZURE_PUBLISH_PROFILE }}",
                 }},
            ])
        elif cloud == "aliyun":
            steps.extend([
                {"name": "Setup Aliyun CLI",
                 "uses": "montagestudio/aliyun-cli@v3",
                 "with": {
                     "access-key-id": "${{ secrets.ALIYUN_ACCESS_KEY_ID }}",
                     "access-key-secret": "${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}",
                     "region": "cn-beijing"}},
                {"name": "Deploy to Aliyun ECS",
                 "run": "aliyun ecs DeployInstance --instance-id ${{ secrets.ALIYUN_INSTANCE_ID }}"},
            ])
        elif cloud == "tencent":
            steps.extend([
                {"name": "Setup Tencent Cloud CLI",
                 "run": "pip install tccli"},
                {"name": "Deploy to Tencent Cloud",
                 "run": "tccli cls deploy --region ap-guangzhou"},
            ])

        return {"runs-on": "ubuntu-latest", "steps": steps}

    def _generate_javascript(self, framework, test, deploy, release, name,
                              node_version):
        workflow = {
            "name": name,
            "on": {"push": {"branches": ["main", "master"]},
                   "pull_request": {"branches": ["main", "master"]}},
        }

        jobs = {}
        test_job = self._build_js_test_job(
            framework, test, deploy, release, node_version
        )
        jobs["test"] = test_job

        if release == "npm":
            jobs["publish"] = self._build_npm_publish_job(node_version)
            jobs["publish"]["needs"] = ["test"]

        if deploy == "docker":
            jobs["docker"] = self._build_docker_job(node_version)
            jobs["docker"]["needs"] = ["test"]

        if deploy in ("azure", "aliyun", "tencent"):
            jobs["deploy"] = self._build_js_cloud_deploy_job(deploy, node_version)
            jobs["deploy"]["needs"] = ["test"]

        workflow["jobs"] = jobs
        return yaml.dump(workflow, default_flow_style=False, sort_keys=False,
                         allow_unicode=True)

    def _build_js_test_job(self, framework, test, deploy, release,
                             node_version):
        steps = [
            {"name": "Checkout code", "uses": "actions/checkout@v4"},
            {
                "name": f"Set up Node.js {node_version}",
                "uses": "actions/setup-node@v4",
                "with": {"node-version": node_version,
                         "cache": "npm"},
            },
            {"name": "Install dependencies", "run": "npm ci"},
        ]

        if test == "jest":
            steps.append({"name": "Run Jest tests", "run": "npm test"})
        elif test == "mocha":
            steps.append({"name": "Run Mocha tests",
                          "run": "npm run test:mocha"})

        return {"runs-on": "ubuntu-latest", "steps": steps}

    def _build_npm_publish_job(self, node_version):
        return {
            "runs-on": "ubuntu-latest",
            "needs": ["test"],
            "steps": [
                {"name": "Checkout code", "uses": "actions/checkout@v4"},
                {
                    "name": f"Set up Node.js {node_version}",
                    "uses": "actions/setup-node@v4",
                    "with": {"node-version": node_version,
                             "registry-url": "https://registry.npmjs.org"},
                },
                {"name": "Publish to npm",
                 "run": "npm publish",
                 "env": {"NODE_AUTH_TOKEN": "${{ secrets.NPM_TOKEN }}"}},
            ],
        }

    def _build_docker_job(self, node_version):
        return {
            "runs-on": "ubuntu-latest",
            "needs": ["test"],
            "steps": [
                {"name": "Checkout code", "uses": "actions/checkout@v4"},
                {"name": "Log in to Docker Hub",
                 "uses": "docker/login-action@v3",
                 "with": {"username": "${{ secrets.DOCKERHUB_USERNAME }}",
                          "password": "${{ secrets.DOCKERHUB_TOKEN }}"}},
                {
                    "name": "Build and push Docker image",
                    "uses": "docker/build-push-action@v5",
                    "with": {
                        "context": ".",
                        "push": True,
                        "tags": "${{ secrets.DOCKERHUB_USERNAME }}/app:latest",
                    },
                },
            ],
        }

    def _build_js_cloud_deploy_job(self, cloud, node_version):
        steps = [
            {"name": "Checkout code", "uses": "actions/checkout@v4"},
            {
                "name": f"Set up Node.js {node_version}",
                "uses": "actions/setup-node@v4",
                "with": {"node-version": node_version},
            },
            {"name": "Install dependencies", "run": "npm ci"},
        ]

        if cloud == "azure":
            steps.extend([
                {"name": "Login to Azure",
                 "uses": "azure/login@v2",
                 "with": {"creds": "${{ secrets.AZURE_CREDENTIALS }}"}},
                {"name": "Deploy to Azure Web App",
                 "uses": "azure/webapps-deploy@v3",
                 "with": {"app-name": "${{ secrets.AZURE_WEBAPP_NAME }}",
                          "slot-name": "production"}},
            ])
        elif cloud == "aliyun":
            steps.extend([
                {"name": "Setup Aliyun CLI",
                 "uses": "montagestudio/aliyun-cli@v3",
                 "with": {"access-key-id": "${{ secrets.ALIYUN_ACCESS_KEY_ID }}",
                          "access-key-secret": "${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}"}},
            ])
        elif cloud == "tencent":
            steps.extend([
                {"name": "Setup Tencent Cloud CLI", "run": "pip install tccli"},
            ])

        return {"runs-on": "ubuntu-latest", "needs": ["test"], "steps": steps}

    def _generate_go(self, framework, test, deploy, release, name, go_version):
        workflow = {
            "name": name,
            "on": {"push": {"branches": ["main", "master"]},
                   "pull_request": {"branches": ["main", "master"]}},
        }

        jobs = {}
        test_job = self._build_go_test_job(framework, test, go_version)
        jobs["test"] = test_job

        if release == "goreleaser":
            jobs["release"] = self._build_goreleaser_job(go_version)
            jobs["release"]["needs"] = ["test"]

        if deploy == "docker":
            jobs["docker"] = self._build_go_docker_job(go_version)
            jobs["docker"]["needs"] = ["test"]

        workflow["jobs"] = jobs
        return yaml.dump(workflow, default_flow_style=False, sort_keys=False,
                         allow_unicode=True)

    def _build_go_test_job(self, framework, test, go_version):
        return {
            "runs-on": "ubuntu-latest",
            "steps": [
                {"name": "Checkout code", "uses": "actions/checkout@v4"},
                {
                    "name": f"Set up Go {go_version}",
                    "uses": "actions/setup-go@v5",
                    "with": {"go-version": go_version},
                },
                {"name": "Run tests", "run": "go test -v ./..."},
            ],
        }

    def _build_goreleaser_job(self, go_version):
        return {
            "runs-on": "ubuntu-latest",
            "needs": ["test"],
            "steps": [
                {"name": "Checkout code", "uses": "actions/checkout@v4"},
                {
                    "name": f"Set up Go {go_version}",
                    "uses": "actions/setup-go@v5",
                    "with": {"go-version": go_version},
                },
                {"name": "Run GoReleaser",
                 "uses": "goreleaser/goreleaser-action@v5",
                 "with": {"version": "latest",
                          "args": "release --clean"},
                 "env": {"GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}"}},
            ],
        }

    def _build_go_docker_job(self, go_version):
        return {
            "runs-on": "ubuntu-latest",
            "needs": ["test"],
            "steps": [
                {"name": "Checkout code", "uses": "actions/checkout@v4"},
                {"name": "Log in to Docker Hub",
                 "uses": "docker/login-action@v3",
                 "with": {"username": "${{ secrets.DOCKERHUB_USERNAME }}",
                          "password": "${{ secrets.DOCKERHUB_TOKEN }}"}},
                {
                    "name": "Build and push Docker image",
                    "uses": "docker/build-push-action@v5",
                    "with": {
                        "context": ".",
                        "push": True,
                        "tags": "${{ secrets.DOCKERHUB_USERNAME }}/app:latest",
                    },
                },
            ],
        }


path: cicd_gen/generator/gitlab_ci.py
content: """GitLab CI workflow generator."""

import yaml
from . import BaseGenerator


class GitLabCIGenerator(BaseGenerator):
    """Generator for GitLab CI/CD pipelines."""

    def generate(self, language, framework=None, test=None, deploy=None,
                 release=None, coverage=None, cloud=None, name="CI",
                 python_version="3.11", node_version="20", go_version="1.22",
                 **kwargs):
        self.validate(language, framework, test, deploy, release)

        if language == "python":
            return self._generate_python(framework, test, coverage, deploy,
                                          cloud, python_version)
        elif language == "javascript":
            return self._generate_javascript(framework, test, deploy, release,
                                             node_version)
        elif language == "go":
            return self._generate_go(framework, test, release, deploy,
                                     go_version)

    def _generate_python(self, framework, test, coverage, deploy, cloud,
                          python_version):
        stages = ["test", "deploy"]
        if coverage:
            stages.insert(1, "coverage")

        image = f"python:{python_version}-slim"

        test_script = []
        if test == "pytest":
            test_script.append("pip install pytest pytest-cov")
            cov = "pytest --cov=. --cov-report=xml" if coverage else "pytest"
            test_script.append(cov)
        elif test == "unittest":
            test_script.append("pip install coverage")
            cov = "python -m coverage run -m unittest discover" if coverage \
                else "python -m unittest discover"
            test_script.append(cov)

        if coverage:
            test_job = {
                "image": image,
                "stage": "coverage",
                "script": test_script + [
                    "coverage xml",
                ],
                "coverage": "/TOTAL.*\\s+(\\d+%)$/",
                "artifacts": {"reports": {"cobertura": "coverage.xml"}},
            }
        else:
            test_job = {"image": image, "stage": "test", "script": test_script}

        deploy_job = None
        if deploy == "docker":
            deploy_job = {
                "image": {"name": "docker:latest", "entrypoint": [""]},
                "stage": "deploy",
                "services": ["docker:dind"],
                "script": [
                    "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY",
                    "docker build -t $CI_REGISTRY_IMAGE:latest .",
                    "docker push $CI_REGISTRY_IMAGE:latest",
                ],
            }
        elif deploy == "k8s":
            deploy_job = {
                "image": {"name": "bitnami/kubectl:latest", "entrypoint": [""]},
                "stage": "deploy",
                "script": [
                    "kubectl apply -f k8s/",
                    "-n default",
                ],
            }
        elif cloud == "azure":
            deploy_job = {
                "image": image,
                "stage": "deploy",
                "script": [
                    "pip install azure-cli",
                    "az login --service-principal -u $AZURE_CLIENT_ID "
                    "-p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID",
                    "az webapp up --name $AZURE_WEBAPP_NAME --location eastus",
                ],
            }

        pipeline = {
            "stages": stages if coverage or deploy else ["test"],
        }
        pipeline["test"] = test_job
        if coverage:
            pipeline["coverage"] = {
                "image": image, "stage": "coverage",
                "script": ["curl -s https://codecov.io/bash | bash"],
                "needs": ["test"],
            }
        if deploy_job:
            deploy_job["needs"] = ["test"]
            pipeline["deploy"] = deploy_job

        return yaml.dump(pipeline, default_flow_style=False, sort_keys=False,
                         allow_unicode=True)

    def _generate_javascript(self, framework, test, deploy, release,
                              node_version):
        stages = ["test"]
        if release or deploy:
            stages.append("deploy")

        image = f"node:{node_version}-slim"

        test_script = []
        if test == "jest":
            test_script.append("npm ci")
            test_script.append("npm test")
        elif test == "mocha":
            test_script.append("npm ci")
            test_script.append("npm run test:mocha")

        test_job = {"image": image, "stage": "test", "script": test_script}

        deploy_job = None
        if release == "npm":
            deploy_job = {
                "image": image,
                "stage": "deploy",
                "script": [
                    "npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN",
                    "npm publish --access public",
                ],
                "only": ["tags"],
            }
        elif deploy == "docker":
            deploy_job = {
                "image": {"name": "docker:latest", "entrypoint": [""]},
                "stage": "deploy",
                "services": ["docker:dind"],
                "script": [
                    "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY",
                    "docker build -t $CI_REGISTRY_IMAGE:latest .",
                    "docker push $CI_REGISTRY_IMAGE:latest",
                ],
            }

        pipeline = {"stages": stages}
        pipeline["test"] = test_job
        if deploy_job:
            deploy_job["needs"] = ["test"]
            pipeline["deploy"] = deploy_job

        return yaml.dump(pipeline, default_flow_style=False, sort_keys=False,
                         allow_unicode=True)

    def _generate_go(self, framework, test, release, deploy, go_version):
        stages = ["test"]
        if release or deploy:
            stages.append("deploy")

        test_job = {
            "image": f"golang:{go_version}-alpine",
            "stage": "test",
            "script": [
                "apk add --no-cache git",
                "go test -v ./...",
            ],
        }

        deploy_job = None
        if release == "goreleaser":
            deploy_job = {
                "image": {"name": "goreleaser/goreleaser:latest", "entrypoint": [""]},
                "stage": "deploy",
                "script": ["goreleaser release --clean --snapshot"],
                "only": ["tags"],
            }
        elif deploy == "docker":
            deploy_job = {
                "image": {"name": "docker:latest", "entrypoint": [""]},
                "stage": "deploy",
                "services": ["docker:dind"],
                "script": [
                    "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY",
                    "docker build -t $CI_REGISTRY_IMAGE:latest .",
                    "docker push $CI_REGISTRY_IMAGE:latest",
                ],
            }

        pipeline = {"stages": stages, "test": test_job}
        if deploy_job:
            deploy_job["needs"] = ["test"]
            pipeline["deploy"] = deploy_job

        return yaml.dump(pipeline, default_flow_style=False, sort_keys=False,
                         allow_unicode=True)


path: cicd_gen/generator/jenkins.py
content: """Jenkins Pipeline generator."""

from . import BaseGenerator


DOCKER_SCRIPT_BLOCK = """
        stage('Build & Push Docker') {{
            steps {{
                script {{
                    def img = docker.build("${{env.DOCKERHUB_USERNAME}}}/app")
                    docker.withRegistry('https://index.docker.io/v1/', 'dockerhub') {{
                        img.push()
                    }}
                }}
            }}
        }}"""


class JenkinsGenerator(BaseGenerator):
    """Generator for Jenkins Pipeline (Jenkinsfile)."""

    def generate(self, language, framework=None, test=None, deploy=None,
                 release=None, coverage=None, cloud=None, name="CI",
                 python_version="3.11", node_version="20", go_version="1.22",
                 **kwargs):
        self.validate(language, framework, test, deploy, release)

        if language == "python":
            return self._generate_python(framework, test, coverage, deploy,
                                         cloud, python_version)
        elif language == "javascript":
            return self._generate_javascript(framework, test, deploy, release,
                                             node_version)
        elif language == "go":
            return self._generate_go(framework, test, release, deploy,
                                      go_version)

    def _generate_python(self, framework, test, coverage, deploy, cloud,
                         python_version):
        test_cmd = self._python_test_cmd(test, coverage)
        deploy_block = self._python_deploy_block(deploy, cloud)

        return (
            "pipeline {\n"
            "    agent { docker { image 'python:" + python_version + "-slim' } }\n"
            "\n"
            "    stages {\n"
            "        stage('Checkout') {\n"
            "            steps { echo 'Cloning repository...' }\n"
            "        }\n"
            "\n"
            "        stage('Install') {\n"
            "            steps {\n"
            "                sh 'pip install -r requirements.txt'\n"
            "            }\n"
            "        }\n"
            "\n"
            "        stage('Test') {\n"
            "            steps {\n"
            "                sh '" + test_cmd + "'\n"
            "            }\n"
            "        }" + deploy_block + "\n"
            "    }\n"
            "}\n"
        )

    def _python_test_cmd(self, test, coverage):
        if test == "pytest":
            return "pytest --cov=. --cov-report=xml" if coverage else "pytest"
        elif test == "unittest":
            return "python -m unittest discover -v"

    def _python_deploy_block(self, deploy, cloud):
        if not deploy:
            return ""

        if deploy == "docker":
            return DOCKER_SCRIPT_BLOCK

        elif deploy == "azure":
            return """
        stage('Deploy to Azure') {
            steps {
                sh 'pip install azure-cli'
                sh 'az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID'
                sh 'az webapp up --name $AZURE_WEBAPP_NAME --location eastus'
            }
        }"""

        elif deploy == "k8s":
            return """
        stage('Deploy to Kubernetes') {
            steps {
                sh 'kubectl apply -f k8s/'
            }
        }"""

        elif deploy == "serverless":
            return """
        stage('Deploy Serverless') {
            steps {
                sh 'npm install -g serverless'
                sh 'serverless deploy'
            }
        }"""

        return ""

    def _generate_javascript(self, framework, test, deploy, release,
                             node_version):
        test_cmd = "npm test" if test == "jest" else "npm run test:mocha"
        stages = self._js_stages(deploy, release)

        return (
            "pipeline {\n"
            "    agent { docker { image 'node:" + node_version + "' } }\n"
            "\n"
            "    stages {\n"
            "        stage('Checkout') {\n"
            "            steps { echo 'Cloning repository...' }\n"
            "        }\n"
            "\n"
            "        stage('Install') {\n"
            "            steps {\n"
            "                sh 'npm ci'\n"
            "            }\n"
            "        }\n"
            "\n"
            "        stage('Test') {\n"
            "            steps {\n"
            "                sh '" + test_cmd + "'\n"
            "            }\n"
            "        }" + stages + "\n"
            "    }\n"
            "}\n"
        )

    def _js_stages(self, deploy, release):
        result = ""
        if release == "npm":
            result += """
        stage('Publish npm') {
            steps {
                sh 'npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN'
                sh 'npm publish --access public'
            }
        }"""

        if deploy == "docker":
            result += DOCKER_SCRIPT_BLOCK

        return result

    def _generate_go(self, framework, test, release, deploy, go_version):
        deploy_block = self._go_deploy_block(release, deploy)

        return (
            "pipeline {\n"
            "    agent { docker { image 'golang:" + go_version + "' } }\n"
            "\n"
            "    stages {\n"
            "        stage('Checkout') {\n"
            "            steps { echo 'Cloning repository...' }\n"
            "        }\n"
            "\n"
            "        stage('Test') {\n"
            "            steps {\n"
            "                sh 'go test -v ./...'\n"
            "            }\n"
            "        }" + deploy_block + "\n"
            "    }\n"
            "}\n"
        )

    def _go_deploy_block(self, release, deploy):
        result = ""
        if release == "goreleaser":
            result += """
        stage('Release with GoReleaser') {
            steps {
                sh 'curl -sL https://git.io/goreleaser | bash'
            }
        }"""

        if deploy == "docker":
            result += DOCKER_SCRIPT_BLOCK

        return result


path: cicd_gen.egg-info/SOURCES.txt
content: setup.py
cicd_gen/__init__.py
cicd_gen/cli.py
cicd_gen.egg-info/PKG-INFO
cicd_gen.egg-info/SOURCES.txt
cicd_gen.egg-info/dependency_links.txt
cicd_gen.egg-info/entry_points.txt
cicd_gen.egg-info/requires.txt
cicd_gen.egg-info/top_level.txt
cicd_gen/generator/__init__.py
cicd_gen/generator/github_actions.py
cicd_gen/generator/gitlab_ci.py
cicd_gen/generator/jenkins.py

path: cicd_gen.egg-info/dependency_links.txt
content: 


path: cicd_gen.egg-info/entry_points.txt
content: [console_scripts]
cicd-gen = cicd_gen.cli:main



path: cicd_gen.egg-info/requires.txt
content: pyyaml


path: cicd_gen.egg-info/top_level.txt
content: cicd_gen


path: setup.py
content: """Setup for cicd-gen CLI."""

from setuptools import setup, find_packages

setup(
    name="cicd-gen",
    version="1.0.0",
    description="Generate production-ready CI/CD workflows",
    author="kay",
    packages=find_packages(),
    include_package_data=True,
    install_requires=["pyyaml"],
    entry_points={
        "console_scripts": [
            "cicd-gen=cicd_gen.cli:main",
        ],
    },
    python_requires=">=3.8",
    classifiers=[
        "Development Status :: 4 - Beta",
        "Intended Audience :: Developers",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "Programming Language :: Python :: 3.12",
    ],
)


path: tests/test_cicd_gen.py
content: """Tests for CI/CD Templates Generator."""
import os
import tempfile
import yaml
import pytest
from pathlib import Path

import sys
sys.path.insert(0, str(Path(__file__).parent.parent))

from cicd_gen.generator.github_actions import GitHubActionsGenerator
from cicd_gen.generator.gitlab_ci import GitLabCIGenerator
from cicd_gen.generator.jenkins import JenkinsGenerator


class TestGitHubActionsGenerator:
    """Test GitHub Actions workflow generation."""

    def test_python_pytest_docker(self):
        """Python + pytest + Docker generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            deploy="docker",
        )
        data = yaml.safe_load(yaml_str)
        assert data["name"] == "CI"
        assert "pytest" in yaml_str
        assert "docker" in yaml_str.lower()

    def test_python_unittest_coverage(self):
        """Python + unittest + coverage generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="unittest",
            coverage=True,
        )
        data = yaml.safe_load(yaml_str)
        assert "coverage" in yaml_str or "coverage" in data.get("jobs", {}).get("test", {}).get("steps", [{}])[0].get("name", "").lower() or "unittest" in yaml_str

    def test_python_docker_azure_deploy(self):
        """Python + Docker + Azure deployment."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            deploy="docker",
            cloud="azure",
        )
        data = yaml.safe_load(yaml_str)
        assert "docker" in yaml_str.lower() or "azure" in yaml_str.lower()

    def test_python_aliyun_deploy(self):
        """Python + Docker + Aliyun deployment."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            deploy="docker",
            cloud="aliyun",
        )
        data = yaml.safe_load(yaml_str)
        assert "aliyun" in yaml_str.lower() or "docker" in yaml_str.lower()

    def test_python_tencent_deploy(self):
        """Python + Docker + Tencent deployment."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            deploy="docker",
            cloud="tencent",
        )
        data = yaml.safe_load(yaml_str)
        assert "tencent" in yaml_str.lower() or "docker" in yaml_str.lower()

    def test_python_k8s_deploy(self):
        """Python + Kubernetes deployment."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            deploy="k8s",
        )
        data = yaml.safe_load(yaml_str)
        assert "k8s" in yaml_str.lower() or "kubernetes" in yaml_str.lower()

    def test_python_serverless_deploy(self):
        """Python + Serverless deployment."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            deploy="serverless",
        )
        data = yaml.safe_load(yaml_str)
        assert "serverless" in yaml_str.lower() or "azure" in yaml_str.lower() or "aws" in yaml_str.lower()

    def test_javascript_jest(self):
        """JavaScript + Jest generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="javascript",
            framework="node",
            test="jest",
        )
        data = yaml.safe_load(yaml_str)
        assert "jest" in yaml_str.lower()

    def test_javascript_docker(self):
        """JavaScript + Docker generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="javascript",
            framework="node",
            test="jest",
            deploy="docker",
        )
        data = yaml.safe_load(yaml_str)
        assert "docker" in yaml_str.lower()

    def test_javascript_npm_publish(self):
        """JavaScript + npm publish generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="javascript",
            framework="node",
            test="jest",
            release="npm",
        )
        data = yaml.safe_load(yaml_str)
        assert "npm" in yaml_str.lower() and ("publish" in yaml_str.lower() or "release" in yaml_str.lower())

    def test_go_goreleaser(self):
        """Go + goreleaser generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="go",
            framework="gin",
            test="go-test",
            release="goreleaser",
        )
        data = yaml.safe_load(yaml_str)
        assert "goreleaser" in yaml_str.lower() or "go" in yaml_str.lower()

    def test_go_docker(self):
        """Go + Docker generates valid workflow."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="go",
            framework="gin",
            test="go-test",
            deploy="docker",
        )
        data = yaml.safe_load(yaml_str)
        assert "docker" in yaml_str.lower() or "build" in yaml_str.lower()

    def test_custom_name_and_version(self):
        """Custom workflow name and Python version."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(
            language="python",
            framework="flask",
            test="pytest",
            name="my-ci",
            python_version="3.10",
            node_version="18",
            go_version="1.21",
        )
        data = yaml.safe_load(yaml_str)
        assert data["name"] == "my-ci"
        assert "3.10" in yaml_str

    def test_yaml_is_valid(self):
        """Generated YAML is parseable."""
        gen = GitHubActionsGenerator()
        yaml_str = gen.generate(language="python", framework="flask", test="pytest")
        data = yaml.safe_load(yaml_str)
        assert isinstance(data, dict)
        assert "on" in data or "name" in data


class TestGitLabCIGenerator:
    """Test GitLab CI workflow generation."""

    def test_python_pytest(self):
        """Python + pytest generates valid GitLab CI."""
        gen = GitLabCIGenerator()
        yaml_str = gen.generate(language="python", framework="flask", test="pytest")
        data = yaml.safe_load(yaml_str)
        assert isinstance(data, dict)

    def test_javascript_jest(self):
        """JavaScript + Jest generates valid GitLab CI."""
        gen = GitLabCIGenerator()
        yaml_str = gen.generate(language="javascript", framework="node", test="jest")
        data = yaml.safe_load(yaml_str)
        assert isinstance(data, dict)

    def test_go_goreleaser(self):
        """Go + goreleaser generates valid GitLab CI."""
        gen = GitLabCIGenerator()
        yaml_str = gen.generate(language="go", framework="gin", test="go-test", release="goreleaser")
        data = yaml.safe_load(yaml_str)
        assert isinstance(data, dict)

    def test_yaml_is_valid(self):
        """Generated YAML is parseable."""
        gen = GitLabCIGenerator()
        yaml_str = gen.generate(language="python", framework="flask", test="pytest")
        data = yaml.safe_load(yaml_str)
        assert isinstance(data, dict)
        assert "stages" in data


class TestJenkinsGenerator:
    """Test Jenkinsfile generation."""

    def test_python_pytest(self):
        """Python + pytest generates valid Jenkinsfile."""
        gen = JenkinsGenerator()
        content = gen.generate(language="python", framework="flask", test="pytest")
        assert "pipeline" in content.lower() or "stage" in content.lower()
        assert "pytest" in content.lower() or "python" in content.lower()

    def test_javascript_jest(self):
        """JavaScript + Jest generates valid Jenkinsfile."""
        gen = JenkinsGenerator()
        content = gen.generate(language="javascript", framework="node", test="jest")
        assert "jest" in content.lower() or "node" in content.lower()

    def test_go_goreleaser(self):
        """Go + goreleaser generates valid Jenkinsfile."""
        gen = JenkinsGenerator()
        content = gen.generate(language="go", framework="gin", test="go-test", release="goreleaser")
        assert "goreleaser" in content.lower() or "go" in content.lower()


class TestCLIArguments:
    """Test CLI argument handling."""

    def test_invalid_language(self):
        """Invalid language raises ValueError."""
        from cicd_gen.generator.github_actions import GitHubActionsGenerator
        gen = GitHubActionsGenerator()
        with pytest.raises(ValueError):
            gen.generate(language="ruby", framework="rails", test="rspec")

    def test_invalid_test_framework(self):
        """Invalid test framework raises ValueError."""
        from cicd_gen.generator.github_actions import GitHubActionsGenerator
        gen = GitHubActionsGenerator()
        with pytest.raises(ValueError):
            gen.generate(language="python", framework="flask", test="rspec")

    def test_invalid_deploy(self):
        """Invalid deploy option raises ValueError."""
        from cicd_gen.generator.github_actions import GitHubActionsGenerator
        gen = GitHubActionsGenerator()
        with pytest.raises(ValueError):
            gen.generate(language="python", framework="flask", test="pytest", deploy="ansible")


class TestOutputFiles:
    """Test file output functionality."""

    def test_output_directory_creation(self, tmp_path):
        """Output directory is created if it doesn't exist."""
        from cicd_gen.generator.github_actions import GitHubActionsGenerator
        gen = GitHubActionsGenerator()
        out_dir = tmp_path / ".github" / "workflows"
        gen.save(
            yaml_str="name: CI\non: [push]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4",
            output_dir=str(out_dir),
            filename="ci.yml",
        )
        assert (out_dir / "ci.yml").exists()


if __name__ == "__main__":
    pytest.main([__file__, "-v"])


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)