CI/CD: GitHub Actions, GitLab CI, Jenkins, caching, blue-green, canary. Triggers: CI, CD, pipeline, GitHub Actions, workflow YAML, release, canary, rollout.
Scanned 5/27/2026
Install to Claude Code
npx -y skills add softspark/ai-toolkit --skill ci-cd-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ci Cd Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/softspark-ci-cd-patterns)More formats (shields.io, HTML) on the badges page.
---
name: ci-cd-patterns
description: "CI/CD: GitHub Actions, GitLab CI, Jenkins, caching, blue-green, canary. Triggers: CI, CD, pipeline, GitHub Actions, workflow YAML, release, canary, rollout."
effort: medium
user-invocable: false
allowed-tools: Read
---
# CI/CD Patterns
## GitHub Actions
### Standard Pipeline
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run typecheck
test:
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.node-version }}
path: coverage/
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run build
```
### Python CI
```yaml
name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -e ".[dev]"
- run: ruff check .
- run: mypy --strict src/
- run: pytest --cov=src --cov-report=xml
```
### Docker Build & Push
```yaml
build-docker:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }}
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
```
## GitLab CI
```yaml
stages:
- lint
- test
- build
- deploy
lint:
stage: lint
image: node:20
cache:
key: $CI_COMMIT_REF_SLUG
paths: [node_modules/]
script:
- npm ci
- npm run lint
test:
stage: test
image: node:20
services:
- postgres:16
variables:
DATABASE_URL: "postgresql://postgres:postgres@postgres/test"
script:
- npm ci
- npm test
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
```
## Docker Multi-stage Builds
### Node.js
```dockerfile
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
# Stage 2: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
```
### Python
```dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir --target=/deps .
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.12/site-packages
COPY src/ ./src/
USER nobody
CMD ["python", "-m", "src.main"]
```
## Caching Strategies
### npm/pnpm
```yaml
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
```
### pip
```yaml
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
```
### Docker Layer Caching
```yaml
- uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
```
## Kubernetes Deployment
### Rolling Update
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
spec:
containers:
- name: api
image: ghcr.io/org/api:latest
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
livenessProbe:
httpGet:
path: /live
port: 3000
initialDelaySeconds: 10
```
## Secret Management
```yaml
# GitHub Actions - use secrets
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
# Never hardcode secrets in pipelines
# Use OIDC for cloud provider auth (no long-lived credentials)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/deploy
aws-region: us-east-1
```
## Release Automation
### Semantic Release
```json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
}
```
### Conventional Commits for Auto-versioning
| Prefix | Version Bump | Example |
|--------|-------------|---------|
| `fix:` | Patch (0.0.x) | `fix: resolve null pointer in auth` |
| `feat:` | Minor (0.x.0) | `feat: add user search endpoint` |
| `feat!:` / `BREAKING CHANGE:` | Major (x.0.0) | `feat!: change API response format` |
## Common Rationalizations
| Excuse | Why It's Wrong |
|--------|----------------|
| "CI is green, ship it" | CI tests the happy path — verify edge cases, security, and performance separately |
| "Manual deploys give us more control" | Manual deploys give you more human error — automate the repeatable parts |
| "We'll set up CI when the project is bigger" | Small projects grow fast — CI debt compounds and retrofitting is painful |
| "Caching isn't worth the complexity" | Uncached builds waste developer time daily — caching pays for itself in a week |
| "Feature flags are over-engineering" | Feature flags decouple deploy from release — they're the cheapest safety net |
## Anti-Patterns
- Secrets in pipeline logs or environment dumps
- No caching (slow builds)
- Running tests only on main (should run on PRs)
- Manual deployments to production
- No rollback strategy
- Skipping linting/type-checking in CI
## Rules
- **MUST** include lint, test, and build stages in every pipeline — deploy-only pipelines defer failure to production
- **MUST** cache dependencies by lockfile hash, not by branch name — branch keys grow unbounded and leak cache across unrelated work
- **NEVER** commit secrets to the pipeline config — use the platform's secret store and reference by name
- **NEVER** echo secrets to job logs; GitHub Actions masks known secrets only if they came from `secrets.*`, not from arbitrary env vars
- **CRITICAL**: every deployment path has a defined rollback — "we'll figure it out" is not a plan and will cost hours during an incident
- **MANDATORY**: PR builds run the same checks as `main` builds; drift between the two hides failures until merge
## Gotchas
- GitHub Actions masks secret values sourced from `${{ secrets.X }}` only. Secrets routed through `env:` and then transformed (base64, JSON) lose the mask and appear in logs verbatim.
- Workflows triggered by `pull_request` from forks run **without** repository secrets by default (security). Jobs that need secrets either gate on `github.event.pull_request.head.repo.full_name == github.repository` or use `pull_request_target` with explicit code-review — the latter is easy to get wrong and allow token theft.
- `actions/cache` restore is best-effort — a cache miss is silent. Jobs that rely on the cache (e.g., skipping tests when nothing changed) must verify cache hits explicitly via the `cache-hit` output.
- GitLab CI's `rules:` and `only:/except:` are mutually exclusive at the job level. Mixing parses only at pipeline run, not at `git push`.
- Semantic-release assumes a linear history. Merge commits on `main` confuse the commit parser and produce no release — stick to squash merges if you rely on it.
## When NOT to Load
- For **generating** a pipeline file for the current project — use `/ci` (this skill is knowledge, not code)
- For one-off deployment commands — use `/deploy`
- For language-specific build toolchain nuances — pair with `/typescript-patterns`, `/python` rules, etc.
- For observability or alerting around deploys — use `/observability-patterns`
- For security scanning (SAST, SCA) steps in pipelines — use `/security-patterns` and `/cve-scan`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!