| name | cicd-deployment |
| description | Build production CI/CD pipelines with GitHub Actions, Docker, zero-downtime blue/green deploys, rollback, and Telegram alerts. Use when building production ci/cd pipelines with github actions, docker, zero-downtime blue/green. |
| domain | development |
| author | oyi77 |
| license | Apache-2.0 |
| subdomain | software-development |
| tags | ["cicd","coding","deployment","docker","github","pipeline","software-engineering","testing"] |
| version | 1.0.0 |
cicd-deployment
When to Use
Trigger phrases:
- "cicd deployment"
- "Help me with cicd deployment"
Use cases:
- When the task matches this skill's domain expertise
When NOT to use:
- For tasks outside this skill's scope
name: cicd-deployment
description: Production-ready CI/CD pipeline and deployment skill. Covers GitHub Actions workflows, systemd user services, Docker, VPS deployment, zero-downtime blue/green strategy, rollback, health checks, and Telegram notifications. Stack: Python/Node.js.
Overview
This skill provides opinionated, fast-to-implement CI/CD patterns for your services. The pipeline follows a strict flow:
lint → test → build → deploy → health-check → notify
Every deployment is automated, auditable, and reversible. Secrets never touch git. Failures auto-notify via Telegram.
Environments
| Env | Branch | Auto-deploy | Approval Required |
|---|
| dev | develop | ✅ Yes | ❌ No |
| staging | staging | ✅ Yes | ❌ No |
| production | main | ✅ Yes (after staging pass) | ✅ Yes (manual gate) |
Secrets Management — CRITICAL
Golden Rule: .env files NEVER go in git. Ever.
Setup Pattern
nano ~/.config/myapp/<service-name>/.env
chmod 600 ~/.config/myapp/<service-name>/.env
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
echo "!.env.example" >> .gitignore
.env.example Template (commit this, not .env)
# .env.example — copy to .env and fill values
APP_NAME=my-service
APP_ENV=production
APP_PORT=8000
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# External APIs
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# Service-specific
SECRET_KEY=
API_KEY=
GitHub Actions Secrets
Store in repo Settings → Secrets and variables → Actions:
SSH_PRIVATE_KEY — deploy key for server access
SERVER_HOST — VPS IP/hostname
SERVER_USER — SSH user (openclaw)
TELEGRAM_BOT_TOKEN — for deployment notifications
TELEGRAM_CHAT_ID — target chat/channel
GitHub Actions Template
- Configure cicd, deployment, domain, relevant, this settings before first use
.github/workflows/deploy.yml
name: CI/CD Deploy Pipeline
on:
push:
branches:
- main
- staging
- develop
pull_request:
branches:
- main
- staging
env:
SERVICE_NAME: my-service
SERVER_USER: openclaw
DEPLOY_PATH: /home/openclaw/services/${{ env.SERVICE_NAME }}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
if: ${{ hashFiles('requirements.txt') != '' }}
[]
[, , , , ]
Deployment Scripts
- Configure cicd, deployment, domain, relevant, this settings before first use
scripts/deploy.sh
#!/bin/bash
set -euo pipefail
SERVICE_NAME="${1:-my-service}"
GIT_SHA="${2:-unknown}"
ENV="${3:-production}"
PORT="${4:-8000}"
DEPLOY_BASE="$HOME/services/$SERVICE_NAME"
ARTIFACT="/tmp/deploy.tar.gz"
BLUE_DIR="$DEPLOY_BASE/blue"
GREEN_DIR="$DEPLOY_BASE/green"
ACTIVE_LINK="$DEPLOY_BASE/active"
LOG_FILE="$DEPLOY_BASE/logs/deploy.log"
mkdir -p "$DEPLOY_BASE/logs" "$BLUE_DIR" "$GREEN_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
log "=== DEPLOY START: $SERVICE_NAME @ $GIT_SHA ($ENV) ==="
if [[ -L "$ACTIVE_LINK" && "$(readlink "")" == ]];
INACTIVE_DIR=
INACTIVE_SERVICE=
ACTIVE_SERVICE=
INACTIVE_DIR=
INACTIVE_SERVICE=
ACTIVE_SERVICE=
-rf /*
tar -xzf -C
-f
ENV_FILE=
[[ -f ]];
[[ -f ]];
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt -q
[[ -f ]];
npm ci --production --silent
systemctl --user stop 2>/dev/null ||
systemctl --user start
MAX_WAIT=30
WAITED=0
curl -sf > /dev/null 2>&1;
2
WAITED=$((WAITED + ))
[[ -ge ]];
systemctl --user stop
1
-sfn
systemctl --user stop 2>/dev/null ||
>
>
scripts/rollback.sh
#!/bin/bash
set -euo pipefail
SERVICE_NAME="${1:-my-service}"
DEPLOY_BASE="$HOME/services/$SERVICE_NAME"
LOG_FILE="$DEPLOY_BASE/logs/deploy.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
log "=== ROLLBACK START: $SERVICE_NAME ==="
ACTIVE_SLOT=$(cat "$DEPLOY_BASE/ACTIVE_SLOT" 2>/dev/null || echo "${SERVICE_NAME}-blue")
ACTIVE_LINK="$DEPLOY_BASE/active"
BLUE_DIR="$DEPLOY_BASE/blue"
GREEN_DIR="$DEPLOY_BASE/green"
if [[ "$ACTIVE_SLOT" == "${SERVICE_NAME}-blue" ]]; then
FALLBACK_DIR="$GREEN_DIR"
FALLBACK_SERVICE="${SERVICE_NAME}-green"
else
FALLBACK_DIR="$BLUE_DIR"
FALLBACK_SERVICE="${SERVICE_NAME}-blue"
systemctl --user start 2>/dev/null || {
1
}
3
systemctl --user is-active --quiet ;
-sfn
>
systemctl --user stop 2>/dev/null ||
2
[[ -f ]];
[[ -n && -n ]];
curl -s -X POST \
-d chat_id= \
-d parse_mode= \
-d text= > /dev/null
scripts/health_check.sh
#!/bin/bash
set -euo pipefail
SERVICE_NAME="${1:-my-service}"
PORT="${2:-8000}"
DEPLOY_BASE="$HOME/services/$SERVICE_NAME"
LOG_FILE="$DEPLOY_BASE/logs/health.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
pass() { log "✅ PASS: $*"; }
fail() { log "❌ FAIL: $*"; exit 1; }
log "=== HEALTH CHECK: $SERVICE_NAME ==="
ACTIVE_SLOT=$(cat "$DEPLOY_BASE/ACTIVE_SLOT" 2>/dev/null || echo "$SERVICE_NAME")
if systemctl --user is-active --quiet "$ACTIVE_SLOT"; then
pass "systemd service $ACTIVE_SLOT is active"
else
fail "systemd service $ACTIVE_SLOT is NOT active"
fi
ss -tlnp | grep -q ;
pass
fail
HTTP_STATUS=$(curl -o /dev/null -s -w --max-time 5 || )
[[ == ]];
pass
[[ == ]];
fail
fail
RECENT_LOGS=$(journalctl --user -u -n 20 --no-pager 2>/dev/null || )
ERROR_COUNT=$( | grep -c -i || )
[[ -gt 5 ]];
| -10
pass
-v ps &>/dev/null;
PID=$(systemctl --user show -p MainPID --value 2>/dev/null || )
[[ -n && != ]];
MEM_MB=$(ps -o rss= -p 2>/dev/null | awk || )
pass
Systemd User Service Templates
Reusable templates for cicd-deployment.
Standard config:
name: cicd-deployment_standard
mode: production
output: results/
format: json
Test config:
name: cicd-deployment_test
mode: development
dry_run: true
verbose: true
FastAPI Service (~/.config/systemd/user/<service-name>.service)
[Unit]
Description=My FastAPI Service — <service-name>
After=network.target
Wants=network.target
[Service]
Type=simple
WorkingDirectory=/home/openclaw/services/<service-name>/active
EnvironmentFile=/home/openclaw/.config/myapp/<service-name>/.env
ExecStart=/home/openclaw/services/<service-name>/active/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
Blue/Green Pair (create two: blue + green)
[Unit]
Description=My <service-name> (Blue Slot)
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/openclaw/services/<service-name>/blue
EnvironmentFile=/home/openclaw/.config/myapp/<service-name>/.env
ExecStart=/home/openclaw/services/<service-name>/blue/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
Enable Services
loginctl enable-linger openclaw
systemctl --user daemon-reload
systemctl --user enable <service-name>-blue <service-name>-green
Docker Templates
Reusable templates for cicd-deployment.
Standard config:
name: cicd-deployment_standard
mode: production
output: results/
format: json
Test config:
name: cicd-deployment_test
mode: development
dry_run: true
verbose: true
Dockerfile — Python/FastAPI
FROM python:3.11-slim
WORKDIR /app
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python deps first (layer cache)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app
COPY . .
# Non-root user
RUN useradd -m appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
Dockerfile — Node.js/Express
FROM node:20-alpine
WORKDIR /app
# Install deps first (layer cache)
COPY package*.json ./
RUN npm ci --production
# Copy app
COPY . .
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/index.js"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
container_name: myapp-${SERVICE_NAME}
restart: unless-stopped
ports:
- "${APP_PORT:-8000}:8000"
env_file:
- .env
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
labels:
- "com.myapp.service=${SERVICE_NAME}"
- "com.myapp.env=${APP_ENV}"
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
-
Service Patterns
- Configure cicd, deployment, domain, relevant, this settings before first use
FastAPI Service — Minimum Viable Structure
my-fastapi-service/
├── main.py # Entry point (app = FastAPI())
├── requirements.txt
├── .env.example
├── .gitignore # Must include .env
├── Dockerfile
├── docker-compose.yml
├── scripts/
│ ├── deploy.sh
│ ├── rollback.sh
│ └── health_check.sh
├── .github/
│ └── workflows/
│ └── deploy.yml
└── tests/
└── test_main.py
Required /health endpoint:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok", "service": "my-service"}
Node.js Express Service — Minimum Viable Structure
my-express-service/
├── src/
│ └── index.js # Entry point
├── package.json
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── scripts/
│ ├── deploy.sh
│ ├── rollback.sh
│ └── health_check.sh
└── .github/
└── workflows/
└── deploy.yml
Required /health endpoint:
const express = require('express');
const app = express();
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'my-service' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Listening on :${PORT}`));
Python Script / Worker Service
For non-HTTP workers (e.g., automation scripts, cron workers):
[Unit]
Description=Background Worker
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/openclaw/services/my-worker/active
EnvironmentFile=/home/openclaw/.config/myapp/my-worker/.env
ExecStart=/home/openclaw/services/my-worker/active/.venv/bin/python worker.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
Health check for workers (process check only):
if systemctl --user is-active --quiet "my-worker"; then
pass "Worker process is running"
else
fail "Worker process is NOT running"
fi
HEARTBEAT="$HOME/services/my-worker/active/heartbeat"
if [[ -f "$HEARTBEAT" ]]; then
LAST_BEAT=$(cat "$HEARTBEAT")
NOW=$(date +%s)
AGE=$((NOW - LAST_BEAT))
if [[ $AGE -lt 300 ]]; then
pass "Worker heartbeat fresh (${AGE}s ago)"
else
fail "Worker heartbeat stale (${AGE}s ago — possible hang)"
fi
fi
Zero-Downtime Blue/Green Flow
Deploy Request
│
▼
[Determine Active Slot]
blue active → deploy to green
green active → deploy to blue
│
▼
[Deploy to INACTIVE slot]
- Unpack artifact
- Copy .env
- Install deps
- Start inactive service
│
▼
[Health Check Inactive]
curl /health → 200?
│
Yes │ No
▼ ▼
[Swap Active Link] [Rollback]
ln -sfn Stop inactive
│ Exit 1
▼
[Stop Old Slot]
│
▼
[Notify Telegram]
✅ Deploy SUCCESS
Rollback Strategy
- Configure cicd, deployment, domain, relevant, this settings before first use
Automatic (on deploy failure)
if ! curl -sf "http://localhost:$PORT/health"; then
log "Health check failed — rolling back"
systemctl --user stop "$INACTIVE_SERVICE"
exit 1
fi
Manual Rollback
ssh openclaw@<server>
cd ~/services/<service-name>
bash scripts/rollback.sh <service-name>
systemctl --user status <service-name>-blue
systemctl --user status <service-name>-green
curl http://localhost:8000/health
Git Revert (for code rollback)
git log --oneline -10
git revert <bad-commit-sha>
git push origin main
Quick Setup Checklist
For a new service:
mkdir -p ~/services/<service-name>/{blue,green,logs}
mkdir -p ~/.config/myapp/<service-name>
nano ~/.config/myapp/<service-name>/.env
chmod 600 ~/.config/myapp/<service-name>/.env
cp *.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable <service-name>-blue <service-name>-green
loginctl enable-linger openclaw
git push origin main
journalctl --user -u <service-name>-blue -f
Common Commands Reference
journalctl --user -u <service-name>-blue -f
journalctl --user -u <service-name>-green --since "10 min ago"
systemctl --user status <service-name>-blue
systemctl --user status <service-name>-green
bash ~/services/<service-name>/scripts/deploy.sh <name> local production 8000
bash ~/services/<service-name>/scripts/rollback.sh <name>
cat ~/services/<service-name>/ACTIVE_SLOT
readlink ~/services/<service-name>/active
cat ~/services/<service-name>/DEPLOYED_SHA
bash ~/services/<service-name>/scripts/health_check.sh <name>
docker compose up -d --build
docker compose logs -f
docker compose ps
docker compose down
Telegram Notification Patterns
Manual alert (from any script):
#!/bin/bash
telegram_notify() {
local message="$1"
local bot_token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
if [[ -z "$bot_token" || -z "$chat_id" ]]; then
echo "WARNING: TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID not set"
return 0
fi
curl -s -X POST "https://api.telegram.org/bot${bot_token}/sendMessage" \
-d chat_id="${chat_id}" \
-d parse_mode="Markdown" \
-d text="${message}" > /dev/null
}
telegram_notify "✅ *Deploy SUCCESS* — \`my-service\` @ $(date)"
telegram_notify "🚨 *Deploy FAILED* — \`my-service\` — manual action required"
Skill for production CI/CD pipelines and blue/green deployment automation. Generic stack examples.
How to Use
- Understand the requirement and existing codebase patterns
- Design the solution with error handling and testability in mind
- Implement incrementally with tests for each change
- Verify against expected outcomes (manual and automated)
- Document usage, edge cases, and integration points
- Review with team before merging to shared branches
When NOT to Use
- Task is about deployment, not development (use deploy skills)
- Task is about code review, not writing (use review skills)
- You need to understand existing code first (use research skills)
- Task is about testing only (use test skills)
- Requirements are unclear (clarify first)
- Task is trivially simple (single line fix)
Red Flags
- Skipping tests to ship faster: Untested code breaks in production when you least expect it
- No error handling in production code: Unhandled errors crash services and lose user data
- Hardcoded configuration values: Hardcoded values prevent environment switching and leak secrets
- Ignoring security implications: Missing input validation, auth bypasses, and injection vulnerabilities
- Over-engineering simple solutions: Premature abstraction adds complexity without proportional benefit
Verification
- All tests pass after code changes (unit, integration, e2e as appropriate)
- Error handling covers documented failure modes and edge cases
- Configuration uses environment variables or config files, not hardcoded values
- Security-sensitive code (auth, payments, API) has explicit review
- Code follows project conventions (naming, patterns, structure)
Process
- Analyze the task requirements
- Apply domain expertise
- Verify output quality
Anti-Rationalization Table
| Rationalization | Reality |
|---|
| "Tests slow me down" | Bugs slow you down 10x more. Tests are speed, not overhead. |
| "I will refactor later" | Technical debt compounds. Refactor as you go. |
| "It works on my machine" | If it is not in CI, it does not work. Ship proof, not claims. |