feat: enhanced the writing skills

This commit is contained in:
duthaho
2026-04-18 18:50:39 +07:00
parent 7fa9a48c6c
commit 09538078e7
136 changed files with 10175 additions and 7947 deletions
+64
View File
@@ -0,0 +1,64 @@
---
name: devops
description: >
Use when containerizing applications, configuring CI/CD pipelines, or deploying to edge — including Docker, Dockerfile, docker-compose, multi-stage builds, GitHub Actions, workflow YAML, matrix builds, workflow_dispatch, Cloudflare Workers, Pages, R2, D1, KV, wrangler, or container registries.
---
# DevOps
## When to Use
- Containerizing applications with Docker or Docker Compose
- Setting up CI/CD pipelines with GitHub Actions
- Deploying to Cloudflare Workers, Pages, R2, D1, or KV
- Optimizing container images, build caching, or deployment workflows
- Configuring wrangler.toml, Durable Objects, or Cloudflare Queues
## When NOT to Use
- Application code without infrastructure concerns — use framework-specific skills
- Database schema changes — use `databases`
- Security auditing — use `owasp`
---
## Quick Reference
| Topic | Reference | Key features |
|-------|-----------|-------------|
| Docker | `references/docker.md` | Dockerfiles, multi-stage builds, Compose, .dockerignore, healthchecks |
| GitHub Actions | `references/github-actions.md` | Workflow YAML, matrix builds, caching, secrets, reusable workflows |
| Cloudflare Workers | `references/cloudflare-workers.md` | Workers, Pages, R2, D1, KV, Durable Objects, wrangler |
---
## Best Practices
1. **Use multi-stage builds** to keep production images small (Docker).
2. **Pin image tags and action versions** — use digests or major version tags, never `latest`.
3. **Order instructions for cache efficiency** — copy dependency manifests before application code (Docker).
4. **Run as non-root** in containers (Docker).
5. **Use caching aggressively** in CI — cache package manager stores and Docker layers (GitHub Actions).
6. **Set minimal permissions** — add a top-level `permissions` block (GitHub Actions).
7. **Extract reusable workflows and composite actions** for shared CI logic (GitHub Actions).
8. **Keep secrets out of logs** — never `echo` a secret (GitHub Actions).
## Common Pitfalls
1. **Bloated images** — using full base images instead of slim/alpine variants (Docker).
2. **Cache invalidation by COPY order** — placing `COPY . .` before `RUN pip install` (Docker).
3. **Secrets baked into layers** (Docker).
4. **Unpinned action versions** (GitHub Actions).
5. **Overly broad triggers** — triggering on every push to every branch (GitHub Actions).
6. **Secret exposure in pull requests from forks** (GitHub Actions).
7. **Using Node.js APIs without `nodejs_compat`** (Cloudflare Workers).
8. **Blocking the event loop** — Workers have strict CPU time limits (Cloudflare Workers).
9. **Using KV for frequently updated data** — eventually consistent with ~60s propagation (Cloudflare Workers).
---
## Related Skills
- `backend-frameworks` — Application code that gets containerized
- `databases` — Database services in Docker Compose
- `owasp` — Security hardening for containers and CI
@@ -1,196 +0,0 @@
# Dockerfile Best Practices Reference
Quick reference for writing efficient, secure, and maintainable Dockerfiles.
## Layer Ordering for Cache Optimization
Order instructions from least-frequently-changed to most-frequently-changed:
```dockerfile
# 1. Base image (changes: rarely)
FROM node:22-slim
# 2. System dependencies (changes: rarely)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# 3. App dependency manifest (changes: sometimes)
COPY package.json pnpm-lock.yaml ./
# 4. Install dependencies (changes: sometimes, cached if manifests unchanged)
RUN pnpm install --frozen-lockfile
# 5. Copy source code (changes: frequently)
COPY . .
# 6. Build step (changes: frequently)
RUN pnpm build
# 7. Runtime config (changes: rarely, but placed last for clarity)
EXPOSE 3000
CMD ["node", "dist/server.js"]
```
**Key rule**: If a layer changes, all subsequent layers are rebuilt. Separate dependency installation from source code copying.
## Multi-Stage Builds
Reduce final image size by separating build and runtime stages.
```
+-------------------+ +-------------------+
| Build Stage | | Runtime Stage |
| | | |
| - Full toolchain | ---> | - Minimal base |
| - Dev deps | | - Only artifacts |
| - Source code | | - No build tools |
| - Build output | | - No source code |
+-------------------+ +-------------------+
~800 MB ~80 MB
```
**Benefits**: Smaller images, faster deploys, reduced attack surface, no build tools in production.
## Base Image Selection
| Image | Size | Use Case | Security | Package Manager |
|-------|------|----------|----------|-----------------|
| **alpine** | ~5 MB | Small images, CLI tools | Good (small surface) | apk |
| **slim** (Debian) | ~80 MB | Most apps (Python, Node) | Good | apt |
| **distroless** | ~20 MB | Production, no shell needed | Excellent (no shell) | None |
| **scratch** | 0 MB | Static Go/Rust binaries | Excellent (nothing) | None |
| **full** (Debian) | ~300 MB | Build stages, debugging | Fair (large surface) | apt |
### Recommendations by Language
| Language | Build Stage | Runtime Stage |
|----------|-------------|---------------|
| **Python** | `python:3.12-slim` | `python:3.12-slim` or `distroless/python3` |
| **Node.js** | `node:22-slim` | `node:22-slim` or `distroless/nodejs22` |
| **Go** | `golang:1.23` | `scratch` or `distroless/static` |
| **Rust** | `rust:1.83` | `scratch` or `distroless/cc` |
| **Java** | `eclipse-temurin:21-jdk` | `eclipse-temurin:21-jre-alpine` |
## Instruction Best Practices
### RUN: Combine and Clean Up
```dockerfile
# BAD: Multiple layers, leftover cache
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# GOOD: Single layer, cache cleaned
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
```
### COPY: Be Specific
```dockerfile
# BAD: Copies everything, including .git, node_modules, etc.
COPY . .
# GOOD: Copy only what's needed (use .dockerignore too)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY src/ ./src/
COPY tsconfig.json ./
```
### .dockerignore Essentials
```
.git
node_modules
__pycache__
.env
*.log
dist
.venv
.pytest_cache
coverage
.DS_Store
```
### USER: Don't Run as Root
```dockerfile
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -s /bin/false appuser
USER appuser
```
### HEALTHCHECK
```dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
```
### ARG vs ENV
| Directive | Available at | Persists in image | Use for |
|-----------|-------------|-------------------|---------|
| `ARG` | Build time only | No | Build-time toggles, versions |
| `ENV` | Build + runtime | Yes | App configuration |
```dockerfile
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
ENV APP_ENV=production
ENV PORT=8000
```
## Security Checklist
| Practice | Command/Example |
|----------|----------------|
| Pin base image digests | `FROM node:22-slim@sha256:abc123...` |
| Run as non-root | `USER appuser` |
| No secrets in layers | Use `--mount=type=secret` or build args |
| Scan for vulnerabilities | `docker scout cves`, `trivy image` |
| Read-only filesystem | `docker run --read-only` |
| Drop capabilities | `docker run --cap-drop ALL` |
| Use `.dockerignore` | Exclude `.env`, `.git`, credentials |
| Minimal base image | Use slim/distroless/scratch |
### Secrets at Build Time (BuildKit)
```dockerfile
# Mount a secret file without baking it into a layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install
# Build command:
# docker build --secret id=npm_token,src=.npmrc .
```
## Image Size Reduction Checklist
1. Use multi-stage builds
2. Choose slim/alpine/distroless base
3. Combine RUN commands
4. Remove package manager caches (`rm -rf /var/lib/apt/lists/*`)
5. Use `.dockerignore`
6. Don't install dev dependencies in runtime stage
7. Remove unnecessary files after build
8. Use `--no-install-recommends` with apt
## Common Pitfalls
| Pitfall | Impact | Fix |
|---------|--------|-----|
| `COPY . .` before `npm install` | No dependency caching | Copy lockfile first, install, then copy source |
| Using `latest` tag | Non-reproducible builds | Pin specific version tags or digests |
| Secrets in `ENV` or `COPY` | Leaked in image layers | Use BuildKit secrets mount |
| Running as root | Security vulnerability | Add `USER` directive |
| No `.dockerignore` | Bloated context, slow builds | Add and maintain `.dockerignore` |
| Installing build tools in final stage | Bloated image | Use multi-stage; build in first stage |
| Not using `--frozen-lockfile` | Non-deterministic installs | Always use lockfile flags |
@@ -1,93 +0,0 @@
# =============================================================================
# Multi-Stage Node.js Dockerfile
# Usage:
# docker build -t myapp .
# docker run -p 3000:3000 myapp
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Install dependencies
# ---------------------------------------------------------------------------
FROM node:22-slim AS deps
# Enable corepack for pnpm support.
RUN corepack enable
WORKDIR /app
# Copy only package manifests first for dependency layer caching.
# Dependencies are only reinstalled when these files change.
COPY package.json pnpm-lock.yaml ./
# Install production and dev dependencies (dev deps needed for build step).
RUN pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Stage 2: Build the application
# ---------------------------------------------------------------------------
FROM node:22-slim AS builder
RUN corepack enable
WORKDIR /app
# Copy dependencies from the deps stage.
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json /app/pnpm-lock.yaml ./
# Copy source code and config files needed for the build.
COPY tsconfig.json ./
COPY src/ ./src/
# COPY public/ ./public/ # Uncomment for Next.js or static assets
# Build the application.
RUN pnpm build
# Remove dev dependencies after build to reduce size.
RUN pnpm prune --prod
# ---------------------------------------------------------------------------
# Stage 3: Production runtime
# ---------------------------------------------------------------------------
FROM node:22-slim AS runtime
# Run as non-root for security.
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /bin/false appuser
WORKDIR /app
# Set production environment.
ENV NODE_ENV=production \
PORT=3000
# Copy only production artifacts from the builder stage.
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# For Next.js standalone output, use instead:
# COPY --from=builder /app/.next/standalone ./
# COPY --from=builder /app/.next/static ./.next/static
# COPY --from=builder /app/public ./public
# Switch to non-root user.
USER appuser
# Expose the application port.
EXPOSE 3000
# Health check -- adjust the endpoint to match your app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then(r => { if (!r.ok) process.exit(1) })" || exit 1
# Run the application.
CMD ["node", "dist/server.js"]
# For Next.js standalone:
# CMD ["node", "server.js"]
# For NestJS:
# CMD ["node", "dist/main.js"]
# For Express with ts-node (dev only, not recommended for production):
# CMD ["npx", "ts-node", "src/server.ts"]
@@ -1,78 +0,0 @@
# =============================================================================
# Multi-Stage Python Dockerfile
# Usage:
# docker build -t myapp .
# docker run -p 8000:8000 myapp
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build dependencies
# ---------------------------------------------------------------------------
# Use slim for building - it has gcc and headers available via apt.
FROM python:3.12-slim AS builder
# Prevent Python from writing .pyc files and enable unbuffered output.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install build-time system dependencies (if any compiled packages need them).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies into a virtual environment.
# Copying requirements first enables Docker layer caching --
# dependencies are only reinstalled when requirements.txt changes.
COPY requirements.txt .
RUN python -m venv /app/.venv \
&& /app/.venv/bin/pip install --no-cache-dir --upgrade pip \
&& /app/.venv/bin/pip install --no-cache-dir -r requirements.txt
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM python:3.12-slim AS runtime
# Prevent .pyc files and enable unbuffered output for logging.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"
# Install only runtime system dependencies (no build tools).
# Add packages here if your app needs them at runtime (e.g., libpq for psycopg).
# RUN apt-get update && apt-get install -y --no-install-recommends \
# libpq5 \
# && rm -rf /var/lib/apt/lists/*
# Create a non-root user for security.
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /bin/false appuser
WORKDIR /app
# Copy the virtual environment from the builder stage.
COPY --from=builder /app/.venv /app/.venv
# Copy application source code.
COPY src/ ./src/
# Switch to non-root user.
USER appuser
# Expose the application port.
EXPOSE 8000
# Health check -- adjust the endpoint to match your app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Run the application.
# For FastAPI/Uvicorn:
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
# For Django/Gunicorn:
# CMD ["gunicorn", "src.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# For a plain script:
# CMD ["python", "-m", "src.main"]
@@ -1,100 +0,0 @@
# =============================================================================
# Development Docker Compose
# Usage:
# docker compose -f docker-compose.dev.yaml up
# docker compose -f docker-compose.dev.yaml down -v # remove volumes too
# =============================================================================
services:
# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------
app:
build:
context: .
dockerfile: Dockerfile
target: builder # Use the build stage for dev (includes dev deps)
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/app_dev
REDIS_URL: redis://redis:6379/0
volumes:
# Mount source code for hot-reload. Exclude node_modules.
- .:/app
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Override CMD for development (hot-reload).
command: ["pnpm", "dev"]
# For Python:
# command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3000", "--reload"]
# ---------------------------------------------------------------------------
# PostgreSQL
# ---------------------------------------------------------------------------
postgres:
image: postgres:17-alpine
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
# Persist data across restarts.
- postgres_data:/var/lib/postgresql/data
# Run init scripts on first start.
# - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
# ---------------------------------------------------------------------------
# Redis
# ---------------------------------------------------------------------------
redis:
image: redis:7-alpine
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# Persist data to disk every 60 seconds if at least 1 key changed.
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
# ---------------------------------------------------------------------------
# Optional: pgAdmin (database GUI)
# ---------------------------------------------------------------------------
# pgadmin:
# image: dpage/pgadmin4:latest
# ports:
# - "5050:80"
# environment:
# PGADMIN_DEFAULT_EMAIL: admin@local.dev
# PGADMIN_DEFAULT_PASSWORD: admin
# depends_on:
# - postgres
# ---------------------------------------------------------------------------
# Optional: Mailpit (email testing)
# ---------------------------------------------------------------------------
# mailpit:
# image: axllent/mailpit:latest
# ports:
# - "8025:8025" # Web UI
# - "1025:1025" # SMTP
volumes:
postgres_data:
redis_data:
@@ -1,250 +0,0 @@
# GitHub Actions Syntax Quick Reference
## Workflow File Structure
```yaml
name: CI # Workflow name (shown in GitHub UI)
on: # Triggers
push:
branches: [main]
pull_request:
branches: [main]
permissions: # Workflow-level permissions
contents: read
env: # Workflow-level environment variables
NODE_ENV: test
jobs:
build: # Job ID
runs-on: ubuntu-latest # Runner
steps:
- uses: actions/checkout@v4 # Action step
- run: echo "Hello" # Shell step
```
## Triggers (on:)
### Common Events
```yaml
on:
push:
branches: [main, "release/**"]
paths: ["src/**", "!src/**/*.test.ts"] # Path filtering
tags: ["v*"]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch: # Manual trigger
inputs:
environment:
type: choice
options: [staging, production]
schedule:
- cron: "0 6 * * 1" # Every Monday at 6am UTC
release:
types: [published]
workflow_call: # Reusable workflow
inputs:
node-version: { type: string, default: "22" }
secrets:
NPM_TOKEN: { required: true }
```
## Jobs
```yaml
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
needs: [lint, test] # Runs after lint AND test succeed
runs-on: ubuntu-latest
steps: [...]
```
### Matrix Strategy
```yaml
jobs:
test:
strategy:
fail-fast: false # Don't cancel other jobs on failure
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22]
exclude:
- os: macos-latest
node: 20
include:
- os: ubuntu-latest
node: 22
coverage: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
```
## Steps
### Action Step
```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history (needed for some tools)
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
```
### Shell Step
```yaml
- name: Run tests
run: npm test
working-directory: ./packages/api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
shell: bash
continue-on-error: true # Don't fail the job
timeout-minutes: 10
```
### Multi-line Commands
```yaml
- run: |
echo "Line 1"
echo "Line 2"
npm run build
```
## Conditionals (if:)
```yaml
# Run only on main branch
- if: github.ref == 'refs/heads/main'
# Run only on pull requests
- if: github.event_name == 'pull_request'
# Run only when previous step failed
- if: failure()
# Always run (even if previous steps failed)
- if: always()
# Run only when a matrix variable is set
- if: matrix.coverage == true
# Run based on changed files (requires dorny/paths-filter or similar)
- if: steps.filter.outputs.backend == 'true'
# Run on specific actor
- if: github.actor != 'dependabot[bot]'
```
## Environment and Secrets
```yaml
jobs:
deploy:
environment:
name: production
url: https://example.com
env:
APP_VERSION: ${{ github.sha }}
steps:
- run: deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # Repository secret
DEPLOY_TOKEN: ${{ vars.DEPLOY_TOKEN }} # Repository variable
```
## Caching
### Built-in Cache (setup actions)
```yaml
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm" # Automatic pnpm cache
```
### Manual Cache
```yaml
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
.mypy_cache
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
```
## Artifacts
### Upload
```yaml
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
```
### Download (in another job)
```yaml
- uses: actions/download-artifact@v4
with:
name: coverage-report
path: ./coverage
```
## Services (Containers)
Define `services:` under a job with `image`, `env`, `ports`, and `options` (for health checks). Common: postgres, redis, mysql.
## Outputs (Passing Data Between Steps/Jobs)
```yaml
# Step output: echo "key=value" >> "$GITHUB_OUTPUT"
# Read in later step: ${{ steps.<step-id>.outputs.key }}
# Job output: declare under jobs.<job>.outputs, read via needs.<job>.outputs.key
```
## Permissions
Common values: `contents: read`, `pull-requests: write`, `issues: write`, `packages: write`, `id-token: write` (OIDC).
## Reusable Workflows
Caller uses `uses: ./.github/workflows/reusable.yaml` with `with:` and `secrets: inherit`. Callee triggers on `workflow_call:` with `inputs:` and `secrets:` definitions.
## Useful Expressions
| Expression | Result |
|-----------|--------|
| `${{ github.sha }}` | Full commit SHA |
| `${{ github.ref_name }}` | Branch or tag name |
| `${{ github.event.pull_request.number }}` | PR number |
| `${{ runner.os }}` | `Linux`, `macOS`, `Windows` |
| `${{ hashFiles('**/lockfile') }}` | SHA256 of files |
| `${{ contains(github.event.head_commit.message, '[skip ci]') }}` | Check commit message |
@@ -1,176 +0,0 @@
# =============================================================================
# Node.js CI Pipeline
# Runs: lint (eslint), type check (tsc), test (vitest), build
# =============================================================================
name: Node CI
on:
push:
branches: [main]
paths:
- "**.ts"
- "**.tsx"
- "**.js"
- "**.jsx"
- "package.json"
- "pnpm-lock.yaml"
- "tsconfig.json"
- ".github/workflows/ci-node.yaml"
pull_request:
branches: [main]
paths:
- "**.ts"
- "**.tsx"
- "**.js"
- "**.jsx"
- "package.json"
- "pnpm-lock.yaml"
- "tsconfig.json"
- ".github/workflows/ci-node.yaml"
permissions:
contents: read
env:
NODE_VERSION: "22"
jobs:
# ---------------------------------------------------------------------------
# Install dependencies (shared across jobs via cache)
# ---------------------------------------------------------------------------
install:
name: Install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Lint with ESLint
# ---------------------------------------------------------------------------
lint:
name: Lint
needs: install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
# ---------------------------------------------------------------------------
# Type check with TypeScript compiler
# ---------------------------------------------------------------------------
type-check:
name: Type Check
needs: install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm tsc --noEmit
# ---------------------------------------------------------------------------
# Test with Vitest
# ---------------------------------------------------------------------------
test:
name: Test
needs: install
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Run tests with coverage
run: pnpm vitest run --coverage --reporter=junit --outputFile=junit.xml
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: junit.xml
retention-days: 7
# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
build:
name: Build
needs: [lint, type-check, test]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
@@ -1,164 +0,0 @@
# =============================================================================
# Python CI Pipeline
# Runs: lint (ruff), type check (mypy), test (pytest), coverage upload
# =============================================================================
name: Python CI
on:
push:
branches: [main]
paths:
- "**.py"
- "requirements*.txt"
- "pyproject.toml"
- ".github/workflows/ci-python.yaml"
pull_request:
branches: [main]
paths:
- "**.py"
- "requirements*.txt"
- "pyproject.toml"
- ".github/workflows/ci-python.yaml"
permissions:
contents: read
env:
PYTHON_VERSION: "3.12"
jobs:
# ---------------------------------------------------------------------------
# Lint with Ruff
# ---------------------------------------------------------------------------
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install ruff
run: pip install ruff
- name: Ruff check (lint)
run: ruff check .
- name: Ruff format (formatting)
run: ruff format --check .
# ---------------------------------------------------------------------------
# Type check with mypy
# ---------------------------------------------------------------------------
type-check:
name: Type Check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install mypy
- name: Run mypy
run: mypy src/ --ignore-missing-imports
# ---------------------------------------------------------------------------
# Test with pytest
# ---------------------------------------------------------------------------
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15
# Uncomment to add a PostgreSQL service for integration tests.
# services:
# postgres:
# image: postgres:17-alpine
# env:
# POSTGRES_USER: test
# POSTGRES_PASSWORD: test
# POSTGRES_DB: test_db
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests with coverage
run: |
pytest \
--cov=src \
--cov-report=xml:coverage.xml \
--cov-report=term-missing \
--junitxml=junit.xml \
-v
env:
PYTHONPATH: ${{ github.workspace }}
# DATABASE_URL: postgresql://test:test@localhost:5432/test_db
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: junit.xml
retention-days: 7
# Uncomment to upload coverage to Codecov.
# - name: Upload to Codecov
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# uses: codecov/codecov-action@v4
# with:
# files: coverage.xml
# token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,545 @@
# DevOps — Cloudflare Workers Patterns
# Cloudflare Workers & Pages
## Overview
Edge-first deployment patterns for Cloudflare's platform. Covers Workers (compute), Pages (static + SSR), R2 (object storage), D1 (SQLite at edge), KV (key-value), Durable Objects (stateful), and Queues (async processing). Focused on the Python/TypeScript stack this kit targets.
## When to Use
- Deploying APIs or full-stack apps to Cloudflare's edge network
- Building serverless functions with Workers
- Deploying Next.js or static sites via Cloudflare Pages
- Using D1 (edge SQLite), R2 (S3-compatible storage), or KV (low-latency reads)
- Implementing real-time coordination with Durable Objects
- Background job processing with Cloudflare Queues
## When NOT to Use
- **Long-running compute** (> 30s CPU) — use traditional servers or containers
- **Heavy database workloads** — D1 is SQLite; use Postgres/Mongo for complex queries
- **GPU/ML inference** (unless using Workers AI) — use dedicated compute
- **Local-only development** — Workers run on V8 isolates, not Node.js
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Worker project structure | § Project Structure below |
| Hono framework on Workers | § Hono Framework below |
| D1 database patterns | § D1 (Edge SQLite) below |
| R2 object storage | § R2 (Object Storage) below |
| KV key-value store | § KV below |
| Durable Objects | § Durable Objects below |
| Pages deployment (Next.js) | § Cloudflare Pages below |
| CI/CD with GitHub Actions | § CI/CD below |
| Wrangler config reference | See `wrangler-patterns.md` in this skill's directory |
---
## Project Structure
```
my-worker/
├── wrangler.toml # Wrangler config (bindings, routes, env)
├── src/
│ ├── index.ts # Entry point (fetch handler)
│ ├── routes/ # Route handlers
│ ├── middleware/ # Auth, CORS, logging
│ ├── services/ # Business logic
│ └── types.ts # Env bindings type
├── migrations/ # D1 migrations
├── test/ # Vitest tests
└── package.json
```
### Entry point
```typescript
// src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/health') {
return Response.json({ status: 'ok' });
}
// Route to handlers...
return new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>;
```
### Type-safe bindings
```typescript
// src/types.ts
export interface Env {
DB: D1Database;
BUCKET: R2Bucket;
CACHE: KVNamespace;
API_KEY: string;
ENVIRONMENT: 'development' | 'staging' | 'production';
}
```
---
## Hono Framework (Recommended)
Hono is the de facto framework for Workers — ultralight (~14KB), type-safe, and built for edge runtimes.
```typescript
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { HTTPException } from 'hono/http-exception';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
type Bindings = {
DB: D1Database;
BUCKET: R2Bucket;
API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use('*', logger());
app.use('*', cors({ origin: ['https://app.example.com'], credentials: true }));
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }));
// Validated endpoint
const createUserSchema = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
});
app.post('/v1/users', zValidator('json', createUserSchema), async (c) => {
const { email, name } = c.req.valid('json');
const result = await c.env.DB
.prepare('INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *')
.bind(crypto.randomUUID(), email, name)
.first();
return c.json(result, 201);
});
// Error handling — RFC 9457 Problem Details
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({
type: `https://api.example.com/problems/${err.status}`,
title: err.message,
status: err.status,
}, err.status);
}
console.error(err);
return c.json({
type: 'https://api.example.com/problems/internal-error',
title: 'Internal server error',
status: 500,
}, 500);
});
export default app;
```
---
## D1 (Edge SQLite)
Cloudflare's serverless SQL database. SQLite at the edge with automatic replication.
### Migrations
```bash
# Create migration
npx wrangler d1 migrations create my-db create-users
# Apply locally
npx wrangler d1 migrations apply my-db --local
# Apply to production
npx wrangler d1 migrations apply my-db --remote
```
```sql
-- migrations/0001_create-users.sql
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT DEFAULT 'member' CHECK(role IN ('admin', 'member', 'viewer')),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_users_email ON users(email);
```
### Querying with prepared statements
```typescript
// Always use prepared statements — never concatenate SQL
async function getUser(db: D1Database, id: string) {
return db.prepare('SELECT * FROM users WHERE id = ?').bind(id).first();
}
async function listUsers(db: D1Database, cursor?: string, limit = 20) {
const stmt = cursor
? db.prepare('SELECT * FROM users WHERE id > ? ORDER BY id LIMIT ?').bind(cursor, limit)
: db.prepare('SELECT * FROM users ORDER BY id LIMIT ?').bind(limit);
return stmt.all();
}
// Batch multiple statements in a transaction
async function transferCredits(db: D1Database, from: string, to: string, amount: number) {
const results = await db.batch([
db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').bind(amount, from),
db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').bind(amount, to),
]);
return results;
}
```
### D1 limitations to know
- **No JOINs across databases** — one D1 database per binding
- **5MB max row size**, 10GB max database
- **Read replicas are automatic** but writes go to a single leader
- **No stored procedures / triggers** — SQLite subset
- **Prepared statements are mandatory** — `db.exec()` with raw SQL is for migrations only
---
## R2 (Object Storage)
S3-compatible object storage without egress fees.
```typescript
// Upload
app.put('/v1/files/:key', async (c) => {
const key = c.req.param('key');
const body = await c.req.arrayBuffer();
const contentType = c.req.header('Content-Type') ?? 'application/octet-stream';
await c.env.BUCKET.put(key, body, {
httpMetadata: { contentType },
customMetadata: { uploadedBy: c.get('userId') },
});
return c.json({ key, size: body.byteLength }, 201);
});
// Download
app.get('/v1/files/:key', async (c) => {
const obj = await c.env.BUCKET.get(c.req.param('key'));
if (!obj) return c.json({ error: 'Not found' }, 404);
return new Response(obj.body, {
headers: {
'Content-Type': obj.httpMetadata?.contentType ?? 'application/octet-stream',
'ETag': obj.etag,
},
});
});
// List with prefix
app.get('/v1/files', async (c) => {
const prefix = c.req.query('prefix') ?? '';
const listed = await c.env.BUCKET.list({ prefix, limit: 100 });
return c.json({ objects: listed.objects.map((o) => ({ key: o.key, size: o.size })) });
});
```
### Presigned URLs for direct upload
```typescript
// Generate a presigned URL so clients upload directly to R2
app.post('/v1/upload-url', async (c) => {
const key = `uploads/${crypto.randomUUID()}`;
// Use the S3-compatible API for presigned URLs
// Requires R2 API token with write access
return c.json({ key, uploadUrl: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com/${BUCKET_NAME}/${key}` });
});
```
---
## KV (Key-Value Store)
Global low-latency reads (~10ms worldwide), eventually consistent writes.
```typescript
// Set with TTL
await c.env.CACHE.put('session:abc123', JSON.stringify(sessionData), {
expirationTtl: 3600, // 1 hour
});
// Get with type safety
const raw = await c.env.CACHE.get('session:abc123');
const session = raw ? JSON.parse(raw) as SessionData : null;
// List keys by prefix
const keys = await c.env.CACHE.list({ prefix: 'session:' });
// Delete
await c.env.CACHE.delete('session:abc123');
```
**Use KV for:** session tokens, feature flags, cached API responses, configuration. **Not for:** frequently updated counters, multi-key transactions (use Durable Objects).
---
## Durable Objects
Stateful, single-instance coordination. Each Durable Object has a unique ID and runs in exactly one location.
```typescript
// src/counter.ts
export class Counter implements DurableObject {
private count = 0;
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment') {
this.count++;
await this.state.storage.put('count', this.count);
return Response.json({ count: this.count });
}
this.count = (await this.state.storage.get<number>('count')) ?? 0;
return Response.json({ count: this.count });
}
}
// In the Worker, route to the Durable Object:
app.post('/v1/counters/:name/increment', async (c) => {
const id = c.env.COUNTER.idFromName(c.req.param('name'));
const stub = c.env.COUNTER.get(id);
const res = await stub.fetch(new Request('https://dummy/increment'));
return c.json(await res.json());
});
```
**Use Durable Objects for:** rate limiting, WebSocket rooms, collaborative editing, distributed locks, shopping carts. **Not for:** read-heavy caching (use KV).
---
## Cloudflare Pages
### Next.js on Pages
```bash
# Deploy Next.js to Cloudflare Pages
npx wrangler pages deploy .next --project-name=my-app
```
Use `@cloudflare/next-on-pages` for full App Router + Server Components support:
```bash
pnpm add @cloudflare/next-on-pages
```
```typescript
// next.config.ts
import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';
if (process.env.NODE_ENV === 'development') {
await setupDevPlatform();
}
const nextConfig = { /* ... */ };
export default nextConfig;
```
### Static site on Pages
```bash
# Build and deploy
pnpm build
npx wrangler pages deploy dist/ --project-name=my-site
```
Pages auto-deploys from GitHub: connect your repo in the Cloudflare dashboard, set the build command and output directory. Preview deploys on every PR.
---
## Wrangler Config
```toml
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
ENVIRONMENT = "production"
# D1 database
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# R2 bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
# KV namespace
[[kv_namespaces]]
binding = "CACHE"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Durable Object
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
# Environment overrides
[env.staging]
vars = { ENVIRONMENT = "staging" }
[env.staging.d1_databases]
binding = "DB"
database_name = "my-db-staging"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
```
**`compatibility_date`** pins your Worker to a specific runtime version. Always set it to a recent date and update periodically. **`nodejs_compat`** enables Node.js built-in APIs (Buffer, crypto, streams) — required for most npm packages.
---
## CI/CD
### GitHub Actions deploy
```yaml
# .github/workflows/deploy.yml
name: Deploy Worker
on:
push:
branches: [main]
pull_request:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: pnpm install
- name: Run tests
run: pnpm test
- name: Apply D1 migrations (production)
if: github.ref == 'refs/heads/main'
run: npx wrangler d1 migrations apply my-db --remote
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Deploy to staging (PR)
if: github.event_name == 'pull_request'
run: npx wrangler deploy --env staging
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
```
### Local development
```bash
# Start local dev server with all bindings (D1, R2, KV, DO)
npx wrangler dev
# With local D1 persistence
npx wrangler dev --persist-to .wrangler/state
```
`wrangler dev` uses Miniflare under the hood — a local simulator for all Cloudflare primitives. Test against real bindings locally before deploying.
---
## Testing
Use **Vitest + Miniflare** (via `@cloudflare/vitest-pool-workers`):
```typescript
// vitest.config.ts
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.toml' },
},
},
},
});
```
```typescript
// test/index.spec.ts
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import worker from '../src/index';
describe('Worker', () => {
it('returns health check', async () => {
const request = new Request('http://localhost/health');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual({ status: 'ok' });
});
});
```
---
## Common Pitfalls
1. **Using Node.js APIs without `nodejs_compat`.** Workers run on V8, not Node.js. Without the flag, `Buffer`, `crypto`, `process` are undefined.
2. **Blocking the event loop.** Workers have strict CPU time limits (10ms free, 30s paid). Heavy computation blocks all concurrent requests. Use `ctx.waitUntil()` for background work.
3. **Ignoring D1's eventually consistent reads.** Writes go to the leader; reads from replicas may lag by seconds. Design for eventual consistency.
4. **Using KV for frequently updated data.** KV is eventually consistent with ~60s propagation. Use Durable Objects for strong consistency.
5. **Not setting `compatibility_date`.** Without it, you get the oldest runtime behavior. Always pin to a recent date.
6. **Forgetting `ctx.waitUntil()`.** Background work (logging, analytics) must be wrapped in `waitUntil()` or it gets killed when the response is sent.
7. **Large Worker bundles.** Workers have a 10MB compressed limit (free: 1MB). Tree-shake aggressively; avoid heavy npm packages.
8. **Not testing locally with Miniflare.** `wrangler dev` simulates all bindings locally. Deploying untested changes to edge = debugging in production.
---
## Related Skills
- `openapi` — API design (Workers APIs benefit from the same conventions)
- `docker` — alternative deployment model (containers vs edge)
- `github-actions` — CI/CD pipeline for deploying Workers
- `typescript` — TypeScript patterns (Workers are TypeScript-first)
- `vitest` — testing Workers with Miniflare pool
@@ -1,8 +1,5 @@
---
name: docker
description: >
Use this skill whenever containerizing applications, writing Dockerfiles, configuring Docker Compose, or optimizing container images. Trigger on keywords like Docker, Dockerfile, container, docker-compose, multi-stage build, image, or container registry. Also applies when setting up local development environments with containers, debugging container networking, or preparing applications for container-based deployment in CI/CD pipelines.
---
# DevOps — Docker Patterns
# Docker
@@ -654,6 +651,6 @@ services:
## Related Skills
- `devops/github-actions` - CI/CD workflows for building and deploying Docker containers
- `security/owasp` - Security best practices for container hardening and vulnerability scanning
- `patterns/logging` — Container logging and log aggregation
- `github-actions` - CI/CD workflows for building and deploying Docker containers
- `owasp` - Security best practices for container hardening and vulnerability scanning
- `logging` — Container logging and log aggregation
@@ -1,8 +1,5 @@
---
name: github-actions
description: >
Use this skill whenever setting up or modifying GitHub Actions CI/CD workflows, automating tests, builds, or deployments on GitHub. Trigger on keywords like GitHub Actions, workflow YAML, CI/CD pipeline, actions/checkout, matrix builds, workflow_dispatch, or .github/workflows. Also applies when configuring caching in workflows, managing GitHub secrets, or troubleshooting failed workflow runs.
---
# DevOps — GitHub Actions Patterns
# GitHub Actions
@@ -799,6 +796,6 @@ jobs:
## Related Skills
- `devops/docker` - Container patterns for building and deploying Dockerized applications in workflows
- `testing/pytest` - Python test configuration for CI pipeline integration
- `testing/vitest` - TypeScript/JavaScript test configuration for CI pipeline integration
- `docker` - Container patterns for building and deploying Dockerized applications in workflows
- `pytest` - Python test configuration for CI pipeline integration
- `vitest` - TypeScript/JavaScript test configuration for CI pipeline integration