Files
gh-slamb2k-mad-skills-dev-flow/skills/start-right/scripts/generate_workflows.py
2025-11-30 08:58:05 +08:00

563 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generate GitHub Actions workflows for CI/CD
This script creates:
- PR validation workflow (runs on feature branches)
- Main branch validation workflow (runs on merge to main)
- Release workflow (versioning, tagging, deployment)
"""
import subprocess
import sys
import json
from pathlib import Path
def run_command(cmd, check=True, capture_output=True):
"""Run a shell command and return the result."""
result = subprocess.run(
cmd,
shell=True,
check=check,
capture_output=capture_output,
text=True
)
return result
def detect_project_type():
"""Detect project type for workflow generation."""
cwd = Path(".")
if (cwd / "package.json").exists():
with open("package.json") as f:
pkg = json.load(f)
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
if "react" in deps or "next" in deps:
return "react"
elif "vite" in deps:
return "vite"
else:
return "node"
elif (cwd / "Cargo.toml").exists():
return "rust"
elif (cwd / "go.mod").exists():
return "go"
elif (cwd / "Dockerfile").exists():
return "docker"
elif any(cwd.glob("*.py")):
return "python"
return "generic"
def create_pr_workflow(project_type, checks):
"""Create PR validation workflow."""
workflow = {
"name": "PR Validation",
"on": {
"pull_request": {
"branches": ["main"]
}
},
"jobs": {
"validate": {
"runs-on": "ubuntu-latest",
"steps": [
{
"name": "Checkout code",
"uses": "actions/checkout@v4"
}
]
}
}
}
# Add project-specific setup
if project_type in ["node", "react", "vite"]:
workflow["jobs"]["validate"]["steps"].extend([
{
"name": "Setup Node.js",
"uses": "actions/setup-node@v4",
"with": {
"node-version": "20",
"cache": "npm"
}
},
{
"name": "Install dependencies",
"run": "npm ci"
}
])
elif project_type == "rust":
workflow["jobs"]["validate"]["steps"].extend([
{
"name": "Setup Rust",
"uses": "actions-rs/toolchain@v1",
"with": {
"toolchain": "stable",
"override": True
}
},
{
"name": "Cache cargo",
"uses": "actions/cache@v4",
"with": {
"path": "~/.cargo\ntarget",
"key": "${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}"
}
}
])
elif project_type == "python":
workflow["jobs"]["validate"]["steps"].extend([
{
"name": "Setup Python",
"uses": "actions/setup-python@v5",
"with": {
"python-version": "3.11",
"cache": "pip"
}
},
{
"name": "Install dependencies",
"run": "pip install -r requirements.txt"
}
])
elif project_type == "go":
workflow["jobs"]["validate"]["steps"].extend([
{
"name": "Setup Go",
"uses": "actions/setup-go@v5",
"with": {
"go-version": "1.21",
"cache": True
}
},
{
"name": "Install dependencies",
"run": "go mod download"
}
])
# Add validation checks based on project type
if "format" in checks:
if project_type in ["node", "react", "vite"]:
workflow["jobs"]["validate"]["steps"].append({
"name": "Check formatting",
"run": "npm run format:check"
})
elif project_type == "rust":
workflow["jobs"]["validate"]["steps"].append({
"name": "Check formatting",
"run": "cargo fmt --check"
})
elif project_type == "python":
workflow["jobs"]["validate"]["steps"].append({
"name": "Check formatting",
"run": "black --check ."
})
if "lint" in checks:
if project_type in ["node", "react", "vite"]:
workflow["jobs"]["validate"]["steps"].append({
"name": "Lint",
"run": "npm run lint"
})
elif project_type == "rust":
workflow["jobs"]["validate"]["steps"].append({
"name": "Lint",
"run": "cargo clippy -- -D warnings"
})
elif project_type == "python":
workflow["jobs"]["validate"]["steps"].append({
"name": "Lint",
"run": "flake8 ."
})
if "type-check" in checks:
if project_type in ["node", "react", "vite"]:
workflow["jobs"]["validate"]["steps"].append({
"name": "Type check",
"run": "npm run type-check"
})
elif project_type == "python":
workflow["jobs"]["validate"]["steps"].append({
"name": "Type check",
"run": "mypy ."
})
if "test" in checks:
if project_type in ["node", "react", "vite"]:
workflow["jobs"]["validate"]["steps"].append({
"name": "Run tests",
"run": "npm test"
})
elif project_type == "rust":
workflow["jobs"]["validate"]["steps"].append({
"name": "Run tests",
"run": "cargo test"
})
elif project_type == "python":
workflow["jobs"]["validate"]["steps"].append({
"name": "Run tests",
"run": "pytest"
})
elif project_type == "go":
workflow["jobs"]["validate"]["steps"].append({
"name": "Run tests",
"run": "go test ./..."
})
if "build" in checks:
if project_type in ["node", "react", "vite"]:
workflow["jobs"]["validate"]["steps"].append({
"name": "Build",
"run": "npm run build"
})
elif project_type == "rust":
workflow["jobs"]["validate"]["steps"].append({
"name": "Build",
"run": "cargo build --release"
})
elif project_type == "go":
workflow["jobs"]["validate"]["steps"].append({
"name": "Build",
"run": "go build -o bin/ ./..."
})
return workflow
def create_main_workflow(project_type, checks):
"""Create main branch workflow with versioning and release."""
workflow = {
"name": "Main Branch CI/CD",
"on": {
"push": {
"branches": ["main"]
}
},
"jobs": {
"validate": {
"runs-on": "ubuntu-latest",
"steps": [
{
"name": "Checkout code",
"uses": "actions/checkout@v4",
"with": {
"fetch-depth": 0 # Full history for versioning
}
}
]
}
}
}
# Reuse PR validation steps
pr_workflow = create_pr_workflow(project_type, checks)
workflow["jobs"]["validate"]["steps"].extend(pr_workflow["jobs"]["validate"]["steps"][1:])
# Add versioning and tagging
workflow["jobs"]["validate"]["steps"].extend([
{
"name": "Bump version and push tag",
"id": "version",
"uses": "anothrNick/github-tag-action@1.67.0",
"env": {
"GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}",
"WITH_V": "true",
"DEFAULT_BUMP": "patch"
}
}
])
# Add release job
workflow["jobs"]["release"] = {
"needs": "validate",
"runs-on": "ubuntu-latest",
"permissions": {
"contents": "write"
},
"steps": [
{
"name": "Checkout code",
"uses": "actions/checkout@v4"
},
{
"name": "Call release workflow",
"uses": "./.github/workflows/release.yml",
"with": {
"version": "${{ needs.validate.outputs.new_tag }}"
}
}
]
}
# Set output
workflow["jobs"]["validate"]["outputs"] = {
"new_tag": "${{ steps.version.outputs.new_tag }}"
}
return workflow
def create_release_workflow(project_type, release_type):
"""Create reusable release workflow based on project and release type."""
workflow = {
"name": "Release",
"on": {
"workflow_call": {
"inputs": {
"version": {
"required": True,
"type": "string"
}
}
}
},
"jobs": {
"release": {
"runs-on": "ubuntu-latest",
"permissions": {
"contents": "write",
"packages": "write"
},
"steps": [
{
"name": "Checkout code",
"uses": "actions/checkout@v4"
}
]
}
}
}
if release_type == "npm":
workflow["jobs"]["release"]["steps"].extend([
{
"name": "Setup Node.js",
"uses": "actions/setup-node@v4",
"with": {
"node-version": "20",
"registry-url": "https://registry.npmjs.org/"
}
},
{
"name": "Install dependencies",
"run": "npm ci"
},
{
"name": "Build",
"run": "npm run build"
},
{
"name": "Publish to npm",
"run": "npm publish",
"env": {
"NODE_AUTH_TOKEN": "${{ secrets.NPM_TOKEN }}"
}
},
{
"name": "Create GitHub Release",
"uses": "softprops/action-gh-release@v1",
"with": {
"tag_name": "${{ inputs.version }}",
"name": "Release ${{ inputs.version }}",
"generate_release_notes": True
}
}
])
elif release_type == "github-pages":
workflow["jobs"]["release"]["steps"].extend([
{
"name": "Setup Node.js",
"uses": "actions/setup-node@v4",
"with": {
"node-version": "20"
}
},
{
"name": "Install dependencies",
"run": "npm ci"
},
{
"name": "Build",
"run": "npm run build"
},
{
"name": "Deploy to GitHub Pages",
"uses": "peaceiris/actions-gh-pages@v3",
"with": {
"github_token": "${{ secrets.GITHUB_TOKEN }}",
"publish_dir": "./dist"
}
},
{
"name": "Create GitHub Release",
"uses": "softprops/action-gh-release@v1",
"with": {
"tag_name": "${{ inputs.version }}",
"body": "Deployed to GitHub Pages: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}",
"generate_release_notes": True
}
}
])
elif release_type == "docker":
workflow["jobs"]["release"]["steps"].extend([
{
"name": "Set up Docker Buildx",
"uses": "docker/setup-buildx-action@v3"
},
{
"name": "Login to GitHub Container Registry",
"uses": "docker/login-action@v3",
"with": {
"registry": "ghcr.io",
"username": "${{ github.actor }}",
"password": "${{ secrets.GITHUB_TOKEN }}"
}
},
{
"name": "Build and push",
"uses": "docker/build-push-action@v5",
"with": {
"context": ".",
"push": True,
"tags": "ghcr.io/${{ github.repository }}:${{ inputs.version }},ghcr.io/${{ github.repository }}:latest"
}
},
{
"name": "Create GitHub Release",
"uses": "softprops/action-gh-release@v1",
"with": {
"tag_name": "${{ inputs.version }}",
"body": "Docker image: `ghcr.io/${{ github.repository }}:${{ inputs.version }}`",
"generate_release_notes": True
}
}
])
elif release_type == "binary":
# For Rust, Go, or other compiled languages
workflow["jobs"]["release"]["steps"].extend([
{
"name": "Build binaries",
"run": "# Add build commands for your project"
},
{
"name": "Create GitHub Release",
"uses": "softprops/action-gh-release@v1",
"with": {
"tag_name": "${{ inputs.version }}",
"files": "bin/*", # Adjust path as needed
"generate_release_notes": True
}
}
])
elif release_type == "skill":
# For Claude Code skills
workflow["jobs"]["release"]["steps"].extend([
{
"name": "Create GitHub Release",
"uses": "softprops/action-gh-release@v1",
"with": {
"tag_name": "${{ inputs.version }}",
"generate_release_notes": True
}
}
])
return workflow
def write_workflow(workflow, filename):
"""Write workflow to .github/workflows directory."""
workflows_dir = Path(".github/workflows")
workflows_dir.mkdir(parents=True, exist_ok=True)
filepath = workflows_dir / filename
import yaml
try:
with open(filepath, "w") as f:
yaml.dump(workflow, f, default_flow_style=False, sort_keys=False)
except ImportError:
# Manual YAML writing if pyyaml not available
import json
yaml_str = json.dumps(workflow, indent=2)
# Basic conversion (not perfect but works for simple cases)
with open(filepath, "w") as f:
f.write(yaml_str.replace('"', '').replace(',', ''))
print(f"✅ Created {filepath}")
def main():
if len(sys.argv) < 2:
print("Usage: generate_workflows.py [--checks format,lint,test,build] [--release npm|github-pages|docker|binary|skill]")
print("\nOptions:")
print(" --checks <list> Comma-separated validation checks")
print(" --release <type> Release strategy")
print("\nRelease types:")
print(" npm Publish to npm registry")
print(" github-pages Deploy to GitHub Pages")
print(" docker Build and push Docker image")
print(" binary Build and release binary artifacts")
print(" skill Claude Code skill (no deployment needed)")
sys.exit(1)
# Parse arguments
checks = ["format", "lint", "test", "build"] # defaults
if "--checks" in sys.argv:
idx = sys.argv.index("--checks")
if idx + 1 < len(sys.argv):
checks = sys.argv[idx + 1].split(",")
release_type = None
if "--release" in sys.argv:
idx = sys.argv.index("--release")
if idx + 1 < len(sys.argv):
release_type = sys.argv[idx + 1]
project_type = detect_project_type()
print("⚙️ Generating GitHub Actions workflows...")
print(f" Project type: {project_type}")
print(f" Checks: {', '.join(checks)}")
if release_type:
print(f" Release type: {release_type}")
print()
# Create PR workflow
pr_workflow = create_pr_workflow(project_type, checks)
write_workflow(pr_workflow, "pr-validation.yml")
# Create main branch workflow
main_workflow = create_main_workflow(project_type, checks)
write_workflow(main_workflow, "main-ci-cd.yml")
# Create release workflow if specified
if release_type:
release_workflow = create_release_workflow(project_type, release_type)
write_workflow(release_workflow, "release.yml")
print("\n✅ GitHub Actions workflows created!")
print("\nWorkflows:")
print(" - pr-validation.yml: Runs on PRs to main")
print(" - main-ci-cd.yml: Runs on merge to main, handles versioning")
if release_type:
print(f" - release.yml: Handles {release_type} deployment")
if __name__ == "__main__":
main()