Files
gh-alekspetrov-navigator/skills/visual-regression/functions/ci_workflow_generator.py
2025-11-29 17:51:59 +08:00

491 lines
12 KiB
Python

#!/usr/bin/env python3
"""
CI/CD Workflow Generator for Visual Regression
Generates GitHub Actions, GitLab CI, and CircleCI workflows for Chromatic/Percy/BackstopJS.
Usage:
python ci_workflow_generator.py <project_root> <ci_platform> <vr_tool>
"""
import json
import os
import sys
from pathlib import Path
from typing import Dict
def detect_node_version(project_root: str) -> str:
"""
Detect Node.js version from .nvmrc or package.json.
Args:
project_root: Project root directory
Returns:
Node version string (default: '20')
"""
# Check .nvmrc
nvmrc = Path(project_root) / '.nvmrc'
if nvmrc.exists():
with open(nvmrc, 'r') as f:
return f.read().strip()
# Check package.json engines.node
package_json = Path(project_root) / 'package.json'
if package_json.exists():
with open(package_json, 'r') as f:
try:
data = json.load(f)
node_version = data.get('engines', {}).get('node')
if node_version:
# Extract version number (handle ">=18.0.0" format)
import re
match = re.search(r'\d+', node_version)
if match:
return match.group(0)
except json.JSONDecodeError:
pass
return '20' # Default
def detect_package_manager(project_root: str) -> str:
"""
Detect package manager from lock files.
Args:
project_root: Project root directory
Returns:
Package manager name ('npm', 'yarn', 'pnpm')
"""
root = Path(project_root)
if (root / 'pnpm-lock.yaml').exists():
return 'pnpm'
elif (root / 'yarn.lock').exists():
return 'yarn'
else:
return 'npm'
def get_install_command(package_manager: str) -> str:
"""Get install command for package manager."""
commands = {
'npm': 'npm ci',
'yarn': 'yarn install --frozen-lockfile',
'pnpm': 'pnpm install --frozen-lockfile'
}
return commands.get(package_manager, 'npm ci')
def detect_branches(project_root: str) -> list:
"""
Detect main branches from git config.
Args:
project_root: Project root directory
Returns:
List of branch names
"""
git_head = Path(project_root) / '.git' / 'HEAD'
if git_head.exists():
with open(git_head, 'r') as f:
content = f.read().strip()
if 'refs/heads/main' in content:
return ['main', 'develop']
elif 'refs/heads/master' in content:
return ['master', 'develop']
return ['main', 'develop']
def generate_github_workflow_chromatic(project_info: Dict) -> str:
"""
Generate GitHub Actions workflow for Chromatic.
Args:
project_info: Project information
Returns:
YAML workflow content
"""
node_version = project_info.get('node_version', '20')
package_manager = project_info.get('package_manager', 'npm')
install_command = get_install_command(package_manager)
branches = project_info.get('branches', ['main', 'develop'])
workflow = f"""name: Visual Regression Tests
on:
push:
branches: {json.dumps(branches)}
pull_request:
branches: ['main']
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Chromatic
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '{node_version}'
cache: '{package_manager}'
- name: Install dependencies
run: {install_command}
- name: Run Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{{{ secrets.CHROMATIC_PROJECT_TOKEN }}}}
exitZeroOnChanges: true
onlyChanged: true
autoAcceptChanges: 'main' # Auto-accept on main branch
"""
return workflow
def generate_github_workflow_percy(project_info: Dict) -> str:
"""
Generate GitHub Actions workflow for Percy.
Args:
project_info: Project information
Returns:
YAML workflow content
"""
node_version = project_info.get('node_version', '20')
package_manager = project_info.get('package_manager', 'npm')
install_command = get_install_command(package_manager)
branches = project_info.get('branches', ['main', 'develop'])
workflow = f"""name: Visual Regression Tests
on:
push:
branches: {json.dumps(branches)}
pull_request:
branches: ['main']
jobs:
percy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '{node_version}'
cache: '{package_manager}'
- name: Install dependencies
run: {install_command}
- name: Build Storybook
run: npm run build-storybook
- name: Run Percy
run: npx percy storybook storybook-static
env:
PERCY_TOKEN: ${{{{ secrets.PERCY_TOKEN }}}}
"""
return workflow
def generate_github_workflow_backstop(project_info: Dict) -> str:
"""
Generate GitHub Actions workflow for BackstopJS.
Args:
project_info: Project information
Returns:
YAML workflow content
"""
node_version = project_info.get('node_version', '20')
package_manager = project_info.get('package_manager', 'npm')
install_command = get_install_command(package_manager)
branches = project_info.get('branches', ['main', 'develop'])
workflow = f"""name: Visual Regression Tests
on:
push:
branches: {json.dumps(branches)}
pull_request:
branches: ['main']
jobs:
backstop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '{node_version}'
cache: '{package_manager}'
- name: Install dependencies
run: {install_command}
- name: Run BackstopJS
run: npm run backstop:test
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v3
with:
name: backstop-results
path: backstop_data/
"""
return workflow
def generate_gitlab_ci_chromatic(project_info: Dict) -> str:
"""
Generate GitLab CI job for Chromatic.
Args:
project_info: Project information
Returns:
YAML job content
"""
node_version = project_info.get('node_version', '20')
install_command = get_install_command(project_info.get('package_manager', 'npm'))
job = f"""# Add to .gitlab-ci.yml
chromatic:
stage: test
image: node:{node_version}
cache:
paths:
- node_modules/
script:
- {install_command}
- npx chromatic --exit-zero-on-changes --only-changed
variables:
CHROMATIC_PROJECT_TOKEN: $CHROMATIC_PROJECT_TOKEN
only:
- main
- develop
- merge_requests
"""
return job
def generate_gitlab_ci_percy(project_info: Dict) -> str:
"""
Generate GitLab CI job for Percy.
Args:
project_info: Project information
Returns:
YAML job content
"""
node_version = project_info.get('node_version', '20')
install_command = get_install_command(project_info.get('package_manager', 'npm'))
job = f"""# Add to .gitlab-ci.yml
percy:
stage: test
image: node:{node_version}
cache:
paths:
- node_modules/
script:
- {install_command}
- npm run build-storybook
- npx percy storybook storybook-static
variables:
PERCY_TOKEN: $PERCY_TOKEN
only:
- main
- develop
- merge_requests
"""
return job
def generate_circleci_config_chromatic(project_info: Dict) -> str:
"""
Generate CircleCI job for Chromatic.
Args:
project_info: Project information
Returns:
YAML job content
"""
node_version = project_info.get('node_version', '20')
install_command = get_install_command(project_info.get('package_manager', 'npm'))
config = f"""# Add to .circleci/config.yml
version: 2.1
executors:
node:
docker:
- image: cimg/node:{node_version}
jobs:
chromatic:
executor: node
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{{{ checksum "package.json" }}}}
- run: {install_command}
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{{{ checksum "package.json" }}}}
- run:
name: Run Chromatic
command: npx chromatic --exit-zero-on-changes --only-changed
environment:
CHROMATIC_PROJECT_TOKEN: $CHROMATIC_PROJECT_TOKEN
workflows:
version: 2
test:
jobs:
- chromatic
"""
return config
def generate_workflow(project_root: str, ci_platform: str, vr_tool: str) -> Dict:
"""
Generate CI/CD workflow for specified platform and VR tool.
Args:
project_root: Project root directory
ci_platform: CI platform ('github', 'gitlab', 'circleci')
vr_tool: VR tool ('chromatic', 'percy', 'backstopjs')
Returns:
Dict with workflow path and content
"""
# Gather project info
project_info = {
'node_version': detect_node_version(project_root),
'package_manager': detect_package_manager(project_root),
'branches': detect_branches(project_root)
}
result = {
'platform': ci_platform,
'vr_tool': vr_tool,
'workflow_path': None,
'workflow_content': None,
'instructions': None
}
# Generate workflow based on platform and tool
if ci_platform == 'github':
workflow_dir = Path(project_root) / '.github' / 'workflows'
workflow_file = 'chromatic.yml' if vr_tool == 'chromatic' else f'{vr_tool}.yml'
result['workflow_path'] = str(workflow_dir / workflow_file)
if vr_tool == 'chromatic':
result['workflow_content'] = generate_github_workflow_chromatic(project_info)
elif vr_tool == 'percy':
result['workflow_content'] = generate_github_workflow_percy(project_info)
elif vr_tool == 'backstopjs':
result['workflow_content'] = generate_github_workflow_backstop(project_info)
result['instructions'] = f"""
GitHub Actions workflow created: {result['workflow_path']}
Next steps:
1. Add secret: Repository Settings → Secrets → Actions
2. Create secret: CHROMATIC_PROJECT_TOKEN (or PERCY_TOKEN)
3. Commit and push this file
4. Workflow will run automatically on push/PR
"""
elif ci_platform == 'gitlab':
result['workflow_path'] = str(Path(project_root) / '.gitlab-ci.yml')
if vr_tool == 'chromatic':
result['workflow_content'] = generate_gitlab_ci_chromatic(project_info)
elif vr_tool == 'percy':
result['workflow_content'] = generate_gitlab_ci_percy(project_info)
result['instructions'] = """
GitLab CI job generated. Add to your .gitlab-ci.yml file.
Next steps:
1. Add variable: Project Settings → CI/CD → Variables
2. Create variable: CHROMATIC_PROJECT_TOKEN (or PERCY_TOKEN)
3. Commit and push .gitlab-ci.yml
4. Pipeline will run automatically
"""
elif ci_platform == 'circleci':
result['workflow_path'] = str(Path(project_root) / '.circleci' / 'config.yml')
if vr_tool == 'chromatic':
result['workflow_content'] = generate_circleci_config_chromatic(project_info)
result['instructions'] = """
CircleCI job generated. Add to your .circleci/config.yml file.
Next steps:
1. Add environment variable in CircleCI project settings
2. Variable name: CHROMATIC_PROJECT_TOKEN
3. Commit and push config.yml
4. Build will run automatically
"""
return result
def main():
"""CLI entry point."""
if len(sys.argv) < 4:
print("Usage: python ci_workflow_generator.py <project_root> <ci_platform> <vr_tool>", file=sys.stderr)
print(" ci_platform: github, gitlab, circleci", file=sys.stderr)
print(" vr_tool: chromatic, percy, backstopjs", file=sys.stderr)
sys.exit(1)
project_root = sys.argv[1]
ci_platform = sys.argv[2].lower()
vr_tool = sys.argv[3].lower()
if ci_platform not in ['github', 'gitlab', 'circleci']:
print(f"Unsupported CI platform: {ci_platform}", file=sys.stderr)
sys.exit(1)
if vr_tool not in ['chromatic', 'percy', 'backstopjs']:
print(f"Unsupported VR tool: {vr_tool}", file=sys.stderr)
sys.exit(1)
result = generate_workflow(project_root, ci_platform, vr_tool)
# Output as JSON
print(json.dumps(result, indent=2))
if __name__ == '__main__':
main()