API / plans /ci-cd-devops.md
sshinmen's picture
Update HF Spaces deployment with latest changes
2e6b65c
|
Raw
History Blame Contribute Delete
26.1 kB
# CI/CD & DevOps Enhancement Plan
## Current State
**Existing**:
- Basic Go build workflow
- Dockerfile for containerization
- Docker Compose setup
- GoReleaser configuration
**Missing**:
- Comprehensive testing in CI
- Security scanning
- Multi-environment deployment
- Infrastructure as Code
- Automated rollback
- Release automation
## Phase 1: Enhanced CI Pipeline (Week 1)
### 1.1 Main CI Workflow
**File**: `.github/workflows/ci.yml`
```yaml
name: CI
on:
push:
branches: [main, develop]
tags: ['v*']
pull_request:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m --out-format=colored-line-number
- name: Check formatting
run: |
fmt_files=$(gofmt -l .)
if [ -n "$fmt_files" ]; then
echo "The following files need formatting:"
echo "$fmt_files"
exit 1
fi
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
minio:
image: minio/minio:latest
env:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- 9000:9000
options: >-
server /data
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Download dependencies
run: go mod download
- name: Run unit tests
run: go test -race -coverprofile=coverage.out ./internal/... ./sdk/...
env:
CGO_ENABLED: 1
- name: Run integration tests
run: go test -v -tags=integration ./test/integration/...
env:
TEST_POSTGRES_URL: postgres://postgres:postgres@localhost:5432/test?sslmode=disable
TEST_REDIS_URL: redis://localhost:6379
TEST_MINIO_ENDPOINT: localhost:9000
TEST_MINIO_ACCESS_KEY: minioadmin
TEST_MINIO_SECRET_KEY: minioadmin
- name: Generate coverage report
run: |
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.out
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
fail_ci_if_error: true
verbose: true
build:
runs-on: ubuntu-latest
needs: [lint, test]
strategy:
matrix:
include:
- platform: linux/amd64
goos: linux
goarch: amd64
- platform: linux/arm64
goos: linux
goarch: arm64
- platform: darwin/amd64
goos: darwin
goarch: amd64
- platform: darwin/arm64
goos: darwin
goarch: arm64
- platform: windows/amd64
goos: windows
goarch: amd64
extension: .exe
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
go build -ldflags "-s -w -X main.version=${{ github.ref_name }} -X main.commit=${{ github.sha }} -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o dist/cliproxy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }} \
./cmd/server
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cliproxy-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/cliproxy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }}
retention-days: 7
docker:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cliproxy
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=,suffix=,format=short
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
DATE=${{ github.event.head_commit.timestamp }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
```
### 1.2 PR Automation
**File**: `.github/workflows/pr-automation.yml`
```yaml
name: PR Automation
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
assign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Auto-assign PR
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
if (pr.user.type === 'User') {
github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
assignees: [pr.user.login]
});
}
size-label:
runs-on: ubuntu-latest
steps:
- name: Label based on size
uses: codelytv/pr-size-labeler@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
xs_label: 'size/xs'
xs_max_size: 50
s_label: 'size/s'
s_max_size: 200
m_label: 'size/m'
m_max_size: 500
l_label: 'size/l'
l_max_size: 1000
xl_label: 'size/xl'
fail_if_xl: false
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v3
with:
fail-on-severity: high
```
## Phase 2: Release Automation (Week 2)
### 2.1 Release Workflow
**File**: `.github/workflows/release.yml`
```yaml
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Generate changelog
id: changelog
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: .github/changelog-config.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
deploy-hf:
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v4
- name: Trigger Hugging Face Space update
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.HF_TOKEN }}" \
https://huggingface.co/api/spaces/${{ secrets.HF_SPACE }}/sync
- name: Wait for deployment
run: |
sleep 60
curl -s https://huggingface.co/api/spaces/${{ secrets.HF_SPACE }} | jq '.runtime.stage'
```
### 2.2 GoReleaser Config
**File**: `.goreleaser.yml`
```yaml
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- id: cliproxy
main: ./cmd/server
binary: cliproxy
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
goarm:
- '7'
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
archives:
- id: cliproxy
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
format_overrides:
- goos: windows
format: zip
files:
- README.md
- LICENSE
- config.example.yaml
dockers:
- image_templates:
- "ghcr.io/echyai/cliproxy:{{ .Tag }}-amd64"
- "echyai/cliproxy:{{ .Tag }}-amd64"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/amd64
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://github.com/echyai/cliproxy
- --label=org.opencontainers.image.source=https://github.com/echyai/cliproxy
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ .Date }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- image_templates:
- "ghcr.io/echyai/cliproxy:{{ .Tag }}-arm64"
- "echyai/cliproxy:{{ .Tag }}-arm64"
dockerfile: Dockerfile
use: buildx
goarch: arm64
build_flag_templates:
- --platform=linux/arm64
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://github.com/echyai/cliproxy
- --label=org.opencontainers.image.source=https://github.com/echyai/cliproxy
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ .Date }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
docker_manifests:
- name_template: "echyai/cliproxy:{{ .Tag }}"
image_templates:
- "echyai/cliproxy:{{ .Tag }}-amd64"
- "echyai/cliproxy:{{ .Tag }}-arm64"
- name_template: "echyai/cliproxy:latest"
image_templates:
- "echyai/cliproxy:{{ .Tag }}-amd64"
- "echyai/cliproxy:{{ .Tag }}-arm64"
- name_template: "ghcr.io/echyai/cliproxy:{{ .Tag }}"
image_templates:
- "ghcr.io/echyai/cliproxy:{{ .Tag }}-amd64"
- "ghcr.io/echyai/cliproxy:{{ .Tag }}-arm64"
- name_template: "ghcr.io/echyai/cliproxy:latest"
image_templates:
- "ghcr.io/echyai/cliproxy:{{ .Tag }}-amd64"
- "ghcr.io/echyai/cliproxy:{{ .Tag }}-arm64"
changelog:
use: github
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
- '^ci:'
- Merge pull request
- Merge branch
release:
github:
owner: echyai
name: cliproxy
draft: false
prerelease: auto
mode: replace
header: |
## CLIProxyAPI {{ .Tag }}
### Docker Images
```bash
docker pull echyai/cliproxy:{{ .Tag }}
docker pull ghcr.io/echyai/cliproxy:{{ .Tag }}
```
footer: |
## Quick Start
```bash
# Download and run
curl -L https://github.com/echyai/cliproxy/releases/download/{{ .Tag }}/cliproxy_Linux_x86_64.tar.gz | tar xz
./cliproxy --config config.yaml
```
```
## Phase 3: Infrastructure as Code (Week 3)
### 3.1 Terraform for AWS
**File**: `infrastructure/terraform/main.tf`
```hcl
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "cliproxy-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "cliproxy-terraform-locks"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "cliproxy"
Environment = var.environment
ManagedBy = "terraform"
}
}
}
# VPC
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${var.project_name}-${var.environment}"
cidr = var.vpc_cidr
azs = var.availability_zones
private_subnets = var.private_subnets
public_subnets = var.public_subnets
enable_nat_gateway = true
single_nat_gateway = var.environment != "production"
enable_dns_hostnames = true
enable_dns_support = true
public_subnet_tags = {
"kubernetes.io/role/elb" = "1"
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1"
}
}
# EKS Cluster
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = "${var.project_name}-${var.environment}"
cluster_version = "1.29"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
control_plane_subnet_ids = module.vpc.intra_subnets
cluster_endpoint_public_access = true
eks_managed_node_groups = {
general = {
desired_size = var.node_desired_size
min_size = var.node_min_size
max_size = var.node_max_size
instance_types = var.node_instance_types
capacity_type = var.environment == "production" ? "ON_DEMAND" : "SPOT"
labels = {
workload = "general"
}
update_config = {
max_unavailable_percentage = 25
}
}
}
# Fargate profiles for serverless workloads
fargate_profiles = {
default = {
name = "default"
selectors = [
{ namespace = "kube-system" },
{ namespace = "default" }
]
}
}
}
# RDS PostgreSQL
module "rds" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.0"
identifier = "${var.project_name}-${var.environment}"
engine = "postgres"
engine_version = "16"
family = "postgres16"
major_engine_version = "16"
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
max_allocated_storage = var.db_max_allocated_storage
db_name = var.db_name
username = var.db_username
port = 5432
multi_az = var.environment == "production"
db_subnet_group_name = module.vpc.database_subnet_group
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = var.environment == "production" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
deletion_protection = var.environment == "production"
enabled_cloudwatch_logs_exports = ["postgresql"]
performance_insights_enabled = var.environment == "production"
}
# ElastiCache Redis
module "redis" {
source = "terraform-aws-modules/elasticache/aws"
version = "~> 1.0"
cluster_id = "${var.project_name}-${var.environment}"
engine = "redis"
engine_version = "7.1"
node_type = var.redis_node_type
num_cache_nodes = 1
subnet_group_name = aws_elasticache_subnet_group.redis.name
security_group_ids = [aws_security_group.redis.id]
at_rest_encryption_enabled = true
transit_encryption_enabled = true
}
# Application Load Balancer
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "~> 9.0"
name = "${var.project_name}-${var.environment}"
load_balancer_type = "application"
vpc_id = module.vpc.vpc_id
subnets = module.vpc.public_subnets
security_groups = [aws_security_group.alb.id]
listeners = {
https = {
port = 443
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.main.arn
fixed_response = {
content_type = "text/plain"
message_body = "OK"
status_code = "200"
}
}
}
}
# Route53
resource "aws_route53_record" "main" {
zone_id = var.route53_zone_id
name = var.domain_name
type = "A"
alias {
name = module.alb.dns_name
zone_id = module.alb.zone_id
evaluate_target_health = true
}
}
```
### 3.2 Kubernetes Manifests
**File**: `infrastructure/k8s/base/deployment.yaml`
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cliproxy
labels:
app: cliproxy
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 10%
selector:
matchLabels:
app: cliproxy
template:
metadata:
labels:
app: cliproxy
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: cliproxy
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: cliproxy
image: echyai/cliproxy:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
env:
- name: CONFIG_TYPE
value: "postgres"
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: cliproxy-db
key: host
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: cliproxy-db
key: port
- name: POSTGRES_DATABASE
valueFrom:
secretKeyRef:
name: cliproxy-db
key: database
- name: POSTGRES_USERNAME
valueFrom:
secretKeyRef:
name: cliproxy-db
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: cliproxy-db
key: password
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir:
sizeLimit: 500Mi
---
apiVersion: v1
kind: Service
metadata:
name: cliproxy
labels:
app: cliproxy
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: cliproxy
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cliproxy
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/...
spec:
rules:
- host: api.cliproxy.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cliproxy
port:
number: 80
```
### 3.3 Kustomize Overlays
**File**: `infrastructure/k8s/overlays/production/kustomization.yaml`
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cliproxy-production
resources:
- ../../base
- hpa.yaml
- pdb.yaml
replicas:
- name: cliproxy
count: 5
images:
- name: echyai/cliproxy
newTag: v1.2.3
patchesStrategicMerge:
- deployment-patch.yaml
configMapGenerator:
- name: cliproxy-config
literals:
- LOG_LEVEL=info
- METRICS_ENABLED=true
secretGenerator:
- name: cliproxy-db
envs:
- .env.db
commonLabels:
environment: production
```
## Phase 4: Monitoring Stack (Week 4)
### 4.1 Prometheus Rules
**File**: `infrastructure/monitoring/prometheus-rules.yaml`
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cliproxy-alerts
spec:
groups:
- name: cliproxy
rules:
- alert: CLProxyHighErrorRate
expr: |
(
sum(rate(cliproxy_http_requests_total{status=~"5.."}[5m]))
/
sum(rate(cliproxy_http_requests_total[5m]))
) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is above 5% for 5 minutes"
- alert: CLProxyHighLatency
expr: |
histogram_quantile(0.99,
sum(rate(cliproxy_http_request_duration_seconds_bucket[5m])) by (le)
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "High latency detected"
description: "P99 latency is above 2 seconds"
- alert: CLProxyProviderErrors
expr: |
sum(rate(cliproxy_provider_requests_total{status="error"}[5m])) by (provider) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Provider {{ $labels.provider }} has high error rate"
- alert: CLProxyCircuitBreakerOpen
expr: cliproxy_circuit_breaker_state == 2
for: 1m
labels:
severity: warning
annotations:
summary: "Circuit breaker is open"
description: "Circuit breaker {{ $labels.name }} has opened"
- alert: CLProxyRateLimitHits
expr: |
sum(rate(cliproxy_rate_limit_hits_total[5m])) > 100
for: 5m
labels:
severity: info
annotations:
summary: "High rate limit hits"
```
### 4.2 Grafana Dashboard
**File**: `infrastructure/monitoring/dashboard.json` (simplified)
```json
{
"dashboard": {
"title": "CLIProxyAPI",
"tags": ["cliproxy", "api"],
"timezone": "UTC",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(cliproxy_http_requests_total[5m])) by (status)"
}
]
},
{
"title": "Latency",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(cliproxy_http_request_duration_seconds_bucket[5m])) by (le))"
}
]
},
{
"title": "Provider Usage",
"type": "graph",
"targets": [
{
"expr": "sum(rate(cliproxy_provider_requests_total[5m])) by (provider)"
}
]
},
{
"title": "Active Connections",
"type": "singlestat",
"targets": [
{
"expr": "cliproxy_active_connections"
}
]
}
]
}
}
```
## Success Metrics
- [ ] Build time < 5 minutes
- [ ] Test coverage > 70%
- [ ] Zero-downtime deployments
- [ ] Automated rollback capability
- [ ] Full observability (logs, metrics, traces)
- [ ] Infrastructure as Code coverage 100%
- [ ] Security scanning in CI
- [ ] Multi-arch container images
## Deployment Flow
```
1. Developer pushes code
↓
2. CI: Lint, Test, Build
↓
3. Docker image built (multi-arch)
↓
4. Security scan (Trivy)
↓
5. Push to registry
↓
6. Deploy to staging (auto)
↓
7. Integration tests
↓
8. Deploy to production (manual approval)
↓
9. Smoke tests
↓
10. Monitor and alert
```