Initial commit
This commit is contained in:
490
skills/visual-regression/functions/ci_workflow_generator.py
Normal file
490
skills/visual-regression/functions/ci_workflow_generator.py
Normal file
@@ -0,0 +1,490 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user