Files
claude-hub/.github/workflows/deploy.yml
Jonathan Flatt c51eba4f0f Add deployment workflow and scripts for self-hosted runner
- Add GitHub Actions deployment workflow for staging and production
- Add deployment scripts for automated deployments
- Add GitHub runner management scripts
- Add staging docker-compose configuration
- Enable automatic deployments on push to main (staging) and version tags (production)
2025-05-23 23:39:33 +00:00

281 lines
8.9 KiB
YAML

name: CI/CD Pipeline
on:
push:
branches:
- main
- develop
tags:
- 'v*.*.*' # Semantic versioning tags (v1.0.0, v2.1.3, etc.)
pull_request:
types: [opened, synchronize, reopened]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ============================================
# CI Jobs - Run on GitHub-hosted runners
# ============================================
test:
name: Run Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Run type checking
run: npm run typecheck
- name: Upload coverage
if: matrix.node-version == '20.x'
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: test
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix={{branch}}-
type=raw,value=staging,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
security-scan:
name: Security Scanning
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ needs.build.outputs.image-tag }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
# ============================================
# CD Jobs - Run on self-hosted runners
# ============================================
deploy-staging:
name: Deploy to Staging
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [build, security-scan]
runs-on: [self-hosted, linux, x64, deployment, webhook-cd]
environment: staging
steps:
- uses: actions/checkout@v4
- name: Create .env file for staging
run: |
cat > .env.staging << EOF
GITHUB_APP_ID_STAGING=${{ secrets.GITHUB_APP_ID_STAGING }}
GITHUB_PRIVATE_KEY_STAGING=${{ secrets.GITHUB_PRIVATE_KEY_STAGING }}
GITHUB_WEBHOOK_SECRET_STAGING=${{ secrets.GITHUB_WEBHOOK_SECRET_STAGING }}
ANTHROPIC_API_KEY_STAGING=${{ secrets.ANTHROPIC_API_KEY_STAGING }}
MCP_SERVER_URL_STAGING=${{ vars.MCP_SERVER_URL_STAGING }}
ALLOWED_ORGS_STAGING=${{ vars.ALLOWED_ORGS_STAGING }}
ALLOWED_REPOS_STAGING=${{ vars.ALLOWED_REPOS_STAGING }}
EOF
- name: Deploy to staging
run: |
export $(cat .env.staging | xargs)
./scripts/deploy/deploy-staging.sh
- name: Clean up
if: always()
run: rm -f .env.staging
- name: Create deployment record
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: 'staging',
required_contexts: [],
auto_merge: false,
description: 'Staging deployment from main branch'
});
- name: Notify deployment status
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Staging deployment ${{ job.status }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
deploy-production:
name: Deploy to Production
if: startsWith(github.ref, 'refs/tags/v')
needs: [build, security-scan]
runs-on: [self-hosted, linux, x64, deployment, webhook-cd]
environment:
name: production
url: https://webhook.yourdomain.com
steps:
- uses: actions/checkout@v4
- name: Validate tag is on main branch
run: |
# Get the commit SHA that the tag points to
TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }})
# Check if this commit exists on main branch
if ! git branch -r --contains $TAG_COMMIT | grep -q "origin/main"; then
echo "Error: Tag ${{ github.ref_name }} is not on the main branch!"
echo "Production deployments must be tagged from the main branch."
exit 1
fi
echo "✓ Tag ${{ github.ref_name }} is on main branch"
- name: Extract version info
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Deploying version: $VERSION"
- name: Create .env file for production
run: |
cat > .env << EOF
GITHUB_APP_ID=${{ secrets.GITHUB_APP_ID }}
GITHUB_PRIVATE_KEY=${{ secrets.GITHUB_PRIVATE_KEY }}
GITHUB_WEBHOOK_SECRET=${{ secrets.GITHUB_WEBHOOK_SECRET }}
ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
MCP_SERVER_URL=${{ vars.MCP_SERVER_URL }}
ALLOWED_ORGS=${{ vars.ALLOWED_ORGS }}
ALLOWED_REPOS=${{ vars.ALLOWED_REPOS }}
DEPLOYMENT_VERSION=${{ steps.version.outputs.version }}
EOF
- name: Deploy to production
run: |
export $(cat .env | xargs)
./scripts/deploy/deploy-production.sh
- name: Clean up
if: always()
run: rm -f .env
- name: Create deployment record
uses: actions/github-script@v7
with:
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.ref,
environment: 'production',
required_contexts: [],
auto_merge: false,
description: `Production deployment ${context.ref.replace('refs/tags/', '')}`
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: 'success',
environment_url: 'https://webhook.yourdomain.com',
description: `Deployed version ${context.ref.replace('refs/tags/', '')}`
});
- name: Create GitHub Release
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: context.ref.replace('refs/tags/', ''),
name: `Release ${context.ref.replace('refs/tags/', '')}`,
body: `Production deployment of ${context.ref.replace('refs/tags/', '')}`,
draft: false,
prerelease: false
});
- name: Notify deployment status
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Production deployment ${{ steps.version.outputs.version }} ${{ job.status }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
fields: repo,message,commit,author,action,eventName,ref,workflow