mirror of
https://github.com/duthaho/claudekit.git
synced 2026-07-28 07:31:00 +03:00
feat: enhanced the writing skills
This commit is contained in:
@@ -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
|
||||
+5
-8
@@ -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
|
||||
Reference in New Issue
Block a user