Files
leo-claude-mktplace/.claude/skills/claude-plugin-developer/references/ci-cd-integration.md

765 lines
17 KiB
Markdown

# CI/CD Integration Guide
Complete guide for automating plugin testing, validation, and deployment.
## GitHub Actions
### Basic Validation Workflow
```yaml
# .github/workflows/validate-plugin.yml
name: Validate Plugin
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Validate plugin manifest
run: |
python scripts/validate_manifest.py .claude-plugin/plugin.json
- name: Check JSON syntax
run: |
find . -name "*.json" -exec jq . {} \; > /dev/null
- name: Test commands
run: |
python scripts/test_commands.py .
- name: Check file permissions
run: |
# Ensure hook scripts are executable
find hooks -name "*.sh" -type f -exec test -x {} \; || exit 1
```
### Advanced Testing Workflow
```yaml
# .github/workflows/test-plugin.yml
name: Test Plugin
on:
push:
branches: [main, develop]
pull_request:
env:
PLUGIN_NAME: my-plugin
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint Markdown
uses: DavidAnson/markdownlint-cli2-action@v14
with:
globs: |
**/*.md
!node_modules
- name: Spell Check
uses: streetsidesoftware/cspell-action@v5
with:
files: |
**/*.md
**/*.json
test-scripts:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run Python tests
run: |
pytest tests/ --cov=scripts --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
test-hooks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Test shell scripts
run: |
# Install shellcheck
sudo apt-get update && sudo apt-get install -y shellcheck
# Check all shell scripts
find . -name "*.sh" -exec shellcheck {} \;
- name: Test hook execution
run: |
# Simulate hook environment
export CHANGED_FILE="test.py"
export FILE_EXTENSION="py"
# Test each hook
for hook in hooks/*.sh; do
if [ -x "$hook" ]; then
echo "Testing $hook..."
timeout 10s "$hook" || echo "Hook $hook failed or timed out"
fi
done
integration:
runs-on: ubuntu-latest
needs: [lint, test-scripts, test-hooks]
steps:
- uses: actions/checkout@v4
- name: Install Claude CLI (mock)
run: |
# In real scenario, install actual Claude CLI
echo "Installing Claude CLI..."
- name: Test plugin installation
run: |
# Mock test - in reality would use Claude CLI
echo "Testing plugin installation..."
- name: Test command execution
run: |
# Mock test - in reality would test commands
echo "Testing command execution..."
```
### Release Workflow
```yaml
# .github/workflows/release.yml
name: Release Plugin
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Validate version tag
run: |
# Extract version from tag
VERSION=${GITHUB_REF#refs/tags/v}
# Check manifest version matches
MANIFEST_VERSION=$(jq -r .version .claude-plugin/plugin.json)
if [ "$VERSION" != "$MANIFEST_VERSION" ]; then
echo "Tag version ($VERSION) doesn't match manifest ($MANIFEST_VERSION)"
exit 1
fi
- name: Create changelog
id: changelog
run: |
# Generate changelog from commits
git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD > CHANGELOG.md
- name: Update marketplace
run: |
# Update marketplace.json if it exists
if [ -f "../marketplace/.claude-plugin/marketplace.json" ]; then
# Update plugin version in marketplace
jq --arg v "$VERSION" \
'.plugins[] | select(.name == "${{ env.PLUGIN_NAME }}").version = $v' \
../marketplace/.claude-plugin/marketplace.json > tmp.json
mv tmp.json ../marketplace/.claude-plugin/marketplace.json
fi
- name: Create Release
uses: softprops/action-gh-release@v1
with:
body_path: CHANGELOG.md
files: |
.claude-plugin/plugin.json
README.md
```
## GitLab CI
### Basic Pipeline
```yaml
# .gitlab-ci.yml
stages:
- validate
- test
- deploy
variables:
PLUGIN_NAME: "my-plugin"
validate:manifest:
stage: validate
image: python:3.11
script:
- python scripts/validate_manifest.py .claude-plugin/plugin.json
only:
- merge_requests
- main
validate:json:
stage: validate
image: stedolan/jq
script:
- find . -name "*.json" -exec jq . {} \;
only:
- merge_requests
- main
test:commands:
stage: test
image: python:3.11
script:
- pip install -r requirements.txt
- python scripts/test_commands.py .
artifacts:
reports:
junit: test-results.xml
test:scripts:
stage: test
image: python:3.11
script:
- pip install pytest pytest-cov
- pytest tests/ --junitxml=test-results.xml
coverage: '/TOTAL.*\s+(\d+%)$/'
deploy:marketplace:
stage: deploy
image: alpine/git
script:
- |
# Update marketplace repository
git clone $MARKETPLACE_REPO marketplace
cd marketplace
# Update plugin entry
# ... update logic ...
git add .
git commit -m "Update $PLUGIN_NAME to $CI_COMMIT_TAG"
git push origin main
only:
- tags
```
### Advanced GitLab Pipeline
```yaml
# .gitlab-ci.yml
include:
- template: Security/SAST.gitlab-ci.yml
- template: Code-Quality.gitlab-ci.yml
stages:
- build
- test
- security
- deploy
- cleanup
.plugin_template:
image: node:18
before_script:
- npm ci --cache .npm --prefer-offline
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
- node_modules/
build:plugin:
extends: .plugin_template
stage: build
script:
- npm run build
- tar -czf plugin.tar.gz .
artifacts:
paths:
- plugin.tar.gz
expire_in: 1 week
test:unit:
extends: .plugin_template
stage: test
script:
- npm test -- --coverage
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test:integration:
stage: test
image: docker:latest
services:
- docker:dind
script:
- docker build -t $PLUGIN_NAME:test .
- docker run --rm $PLUGIN_NAME:test npm run test:integration
security:dependencies:
stage: security
image: node:18
script:
- npm audit --production
allow_failure: true
deploy:staging:
stage: deploy
script:
- echo "Deploying to staging marketplace..."
environment:
name: staging
url: https://staging.marketplace.example.com
only:
- develop
deploy:production:
stage: deploy
script:
- echo "Deploying to production marketplace..."
environment:
name: production
url: https://marketplace.example.com
only:
- tags
when: manual
```
## Jenkins Pipeline
### Jenkinsfile
```groovy
// Jenkinsfile
pipeline {
agent any
environment {
PLUGIN_NAME = 'my-plugin'
PYTHON = 'python3'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Validate') {
parallel {
stage('Manifest') {
steps {
sh '${PYTHON} scripts/validate_manifest.py .claude-plugin/plugin.json'
}
}
stage('JSON Files') {
steps {
sh 'find . -name "*.json" -exec jq . {} \\;'
}
}
stage('Markdown') {
steps {
sh 'npx markdownlint-cli2 "**/*.md"'
}
}
}
}
stage('Test') {
steps {
sh '''
${PYTHON} -m pip install -r requirements.txt
${PYTHON} -m pytest tests/ --junitxml=test-results.xml
'''
}
post {
always {
junit 'test-results.xml'
}
}
}
stage('Security Scan') {
steps {
sh '''
# Run security scans
${PYTHON} -m pip install safety
safety check
# Check for secrets
npx @secretlint/quick-start "**/*"
'''
}
}
stage('Build') {
when {
tag pattern: "v\\d+\\.\\d+\\.\\d+", comparator: "REGEXP"
}
steps {
sh '''
# Package plugin
tar -czf ${PLUGIN_NAME}-${TAG_NAME}.tar.gz .
'''
archiveArtifacts artifacts: '*.tar.gz'
}
}
stage('Deploy') {
when {
tag pattern: "v\\d+\\.\\d+\\.\\d+", comparator: "REGEXP"
}
steps {
input message: 'Deploy to marketplace?'
sh '''
# Deploy to marketplace
echo "Deploying version ${TAG_NAME}"
'''
}
}
}
post {
success {
slackSend(
color: 'good',
message: "Plugin ${PLUGIN_NAME} build successful: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
color: 'danger',
message: "Plugin ${PLUGIN_NAME} build failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
)
}
}
}
```
## CircleCI Configuration
### .circleci/config.yml
```yaml
version: 2.1
orbs:
python: circleci/python@2.1.1
node: circleci/node@5.1.0
executors:
plugin-executor:
docker:
- image: cimg/python:3.11-node
working_directory: ~/plugin
jobs:
validate:
executor: plugin-executor
steps:
- checkout
- run:
name: Validate Manifest
command: python scripts/validate_manifest.py .claude-plugin/plugin.json
- run:
name: Check JSON
command: |
sudo apt-get update && sudo apt-get install -y jq
find . -name "*.json" -exec jq . {} \;
test:
executor: plugin-executor
steps:
- checkout
- python/install-packages:
pkg-manager: pip
pip-dependency-file: requirements.txt
- run:
name: Run Tests
command: |
pytest tests/ --junitxml=test-results/junit.xml
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
security:
executor: plugin-executor
steps:
- checkout
- run:
name: Security Scan
command: |
pip install safety
safety check
deploy:
executor: plugin-executor
steps:
- checkout
- run:
name: Deploy to Marketplace
command: |
echo "Deploying to marketplace..."
workflows:
plugin-pipeline:
jobs:
- validate
- test:
requires:
- validate
- security:
requires:
- validate
- deploy:
requires:
- test
- security
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
```
## Testing Strategies
### Unit Tests
```python
# tests/test_commands.py
import pytest
from pathlib import Path
import json
class TestCommands:
def test_all_commands_have_metadata(self):
"""Ensure all command files have proper metadata."""
commands_dir = Path("commands")
for md_file in commands_dir.rglob("*.md"):
with open(md_file) as f:
content = f.read()
assert content.startswith("---"), f"{md_file} missing frontmatter"
assert "_type: command" in content, f"{md_file} missing _type"
assert "_command:" in content, f"{md_file} missing _command"
assert "_description:" in content, f"{md_file} missing _description"
def test_manifest_valid(self):
"""Test plugin manifest is valid."""
with open(".claude-plugin/plugin.json") as f:
manifest = json.load(f)
assert "name" in manifest
assert "version" in manifest
assert "description" in manifest
assert "author" in manifest
```
### Integration Tests
```python
# tests/test_integration.py
import subprocess
import tempfile
import shutil
from pathlib import Path
def test_plugin_installation():
"""Test plugin can be installed."""
with tempfile.TemporaryDirectory() as tmpdir:
# Copy plugin to temp directory
plugin_dir = Path(tmpdir) / "test-plugin"
shutil.copytree(".", plugin_dir)
# Mock installation test
result = subprocess.run(
["python", "scripts/validate_manifest.py",
str(plugin_dir / ".claude-plugin/plugin.json")],
capture_output=True
)
assert result.returncode == 0
```
## Deployment Strategies
### Blue-Green Deployment
```yaml
# Deploy to staging first, then swap
deploy:staging:
stage: deploy
script:
- deploy_to_marketplace staging
- run_smoke_tests staging
environment:
name: staging
deploy:production:
stage: deploy
script:
- swap_marketplace_slots staging production
when: manual
only:
- tags
```
### Canary Releases
```yaml
deploy:canary:
stage: deploy
script:
- |
# Deploy to 10% of users
update_marketplace_config canary 0.1
# Monitor for 1 hour
sleep 3600
# Check metrics
if check_canary_metrics; then
update_marketplace_config canary 1.0
else
rollback_canary
fi
```
### Rollback Strategy
```bash
#!/bin/bash
# scripts/rollback.sh
PLUGIN_NAME="my-plugin"
PREVIOUS_VERSION=$(git describe --tags --abbrev=0 HEAD^)
echo "Rolling back $PLUGIN_NAME to $PREVIOUS_VERSION"
# Restore previous version
git checkout "v$PREVIOUS_VERSION"
# Update marketplace
update_marketplace_version "$PLUGIN_NAME" "$PREVIOUS_VERSION"
# Notify team
send_notification "Rolled back $PLUGIN_NAME to $PREVIOUS_VERSION"
```
## Monitoring & Alerts
### Health Checks
```yaml
# monitoring/health_check.yml
checks:
- name: plugin_availability
url: https://marketplace.example.com/api/plugins/my-plugin
interval: 5m
- name: command_execution
command: claude /my-plugin health-check
interval: 10m
- name: version_check
script: |
CURRENT=$(claude plugin list | grep my-plugin | awk '{print $2}')
EXPECTED=$(cat .claude-plugin/plugin.json | jq -r .version)
[ "$CURRENT" = "$EXPECTED" ]
interval: 1h
```
### Metrics Collection
```python
# scripts/collect_metrics.py
import json
import requests
from datetime import datetime
def collect_plugin_metrics():
metrics = {
"timestamp": datetime.now().isoformat(),
"plugin": "my-plugin",
"installations": get_installation_count(),
"daily_active_users": get_dau(),
"command_usage": get_command_stats(),
"error_rate": get_error_rate()
}
# Send to monitoring service
requests.post(
"https://metrics.example.com/api/plugins",
json=metrics
)
```
## Best Practices
1. **Version Everything**
- Tag releases with semantic versions
- Keep changelog updated
- Version lock dependencies
2. **Automate Testing**
- Run tests on every commit
- Block merges without passing tests
- Include security scanning
3. **Progressive Rollout**
- Deploy to staging first
- Use canary releases for major changes
- Have rollback plan ready
4. **Monitor Everything**
- Track installation success rate
- Monitor command execution time
- Alert on error spikes
5. **Documentation**
- Update docs with releases
- Include migration guides
- Document breaking changes