feat: improved the Claude Kit as a plugin

This commit is contained in:
duthaho
2026-04-19 14:09:14 +07:00
parent 3103a8da1b
commit d1a6d2a2bc
186 changed files with 771 additions and 1691 deletions
+66
View File
@@ -0,0 +1,66 @@
---
name: devops
description: >
Use when containerizing applications, configuring CI/CD pipelines, deploying to environments, 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, container registries, or deployment workflows (staging, production, health checks, smoke tests).
---
# 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
- Deploying applications to staging or production environments
- Running pre-deploy checks (build, tests, security audit)
- 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
@@ -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
+656
View File
@@ -0,0 +1,656 @@
# DevOps — Docker Patterns
# Docker
## When to Use
- Containerizing applications
- Local development environments
- CI/CD pipelines
## When NOT to Use
- Serverless-only deployments where containers are not part of the architecture (e.g., pure AWS Lambda, Cloudflare Workers)
- Local development without containers where native tooling is preferred
- Simple scripts or utilities that do not need isolation or reproducible environments
---
## Core Patterns
### 1. Multi-Stage Builds
Multi-stage builds separate build-time dependencies from the runtime image, producing
smaller, more secure containers.
#### Python (builder + slim runtime)
```dockerfile
# ---- Build stage ----
FROM python:3.12-slim AS builder
WORKDIR /build
# Install build-only dependencies (gcc, etc.) needed by some wheels
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ---- Runtime stage ----
FROM python:3.12-slim
WORKDIR /app
# Copy only the installed packages from the builder
COPY --from=builder /install /usr/local
# Copy application code
COPY src/ ./src/
COPY main.py .
# Run as non-root
RUN addgroup --system app && adduser --system --ingroup app app
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
#### Node.js (build + nginx/alpine)
```dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first for layer caching
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Copy source and build
COPY tsconfig.json ./
COPY src/ ./src/
COPY public/ ./public/
RUN pnpm build
# ---- Runtime stage (static site served by nginx) ----
FROM nginx:1.27-alpine
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Run as non-root
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
```
#### Node.js (API server with alpine runtime)
```dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY tsconfig.json ./
COPY src/ ./src/
RUN pnpm build
# Prune dev dependencies for a lighter production node_modules
RUN pnpm prune --prod
# ---- Runtime stage ----
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
```
#### Go (build + scratch)
```dockerfile
# ---- Build stage ----
FROM golang:1.22-alpine AS builder
WORKDIR /build
# Download dependencies first for caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source and build a static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# ---- Runtime stage (scratch = empty image) ----
FROM scratch
# Copy CA certificates for HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy the static binary
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
```
---
### 2. Docker Compose for Development
A full-featured Compose file with services, volumes, networks, healthchecks, and
environment variable management.
```yaml
services:
app:
build:
context: .
dockerfile: Dockerfile
target: builder # Use builder stage for dev with hot-reload
ports:
- "3000:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://user:pass@db:5432/app
REDIS_URL: redis://redis:6379
env_file:
- .env.local # Local overrides (gitignored)
volumes:
- .:/app # Bind-mount source for hot-reload
- /app/node_modules # Anonymous volume to preserve node_modules
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- backend
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- backend
worker:
build:
context: .
dockerfile: Dockerfile.worker
environment:
DATABASE_URL: postgresql://user:pass@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
backend:
driver: bridge
```
---
### 3. Layer Caching
Docker caches each layer. If a layer has not changed, every layer after it is also
cached. Order instructions from least-frequently-changed to most-frequently-changed.
#### Optimal instruction order
```dockerfile
FROM python:3.12-slim
WORKDIR /app
# 1. System dependencies (rarely change)
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# 2. Dependency manifests (change when adding packages)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. Application code (changes most often)
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
```
#### .dockerignore patterns
Always include a `.dockerignore` to keep the build context small and avoid leaking
secrets into layers.
```
# Version control
.git
.gitignore
# Dependencies (rebuilt inside container)
node_modules
__pycache__
*.pyc
.venv
venv
# Build output
dist
build
*.egg-info
# IDE and editor files
.vscode
.idea
*.swp
*.swo
# Environment and secrets
.env
.env.*
*.pem
*.key
# Docker files (not needed in context)
Dockerfile*
docker-compose*
.dockerignore
# Documentation and misc
README.md
CHANGELOG.md
LICENSE
docs/
```
---
### 4. Health Checks
Health checks let Docker (and orchestrators like Compose/Swarm/K8s) know when a
container is actually ready to serve traffic.
#### HTTP health check with curl
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
```
#### HTTP health check with wget (alpine images without curl)
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
```
#### TCP port check (for non-HTTP services)
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD nc -z localhost 5432 || exit 1
```
#### Python-native check (no extra binaries needed)
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
```
**Parameter reference:**
| Parameter | Description | Default |
|------------------|--------------------------------------------------|---------|
| `--interval` | Time between checks | 30s |
| `--timeout` | Max time for a single check | 30s |
| `--start-period` | Grace period before checks count as failures | 0s |
| `--retries` | Consecutive failures before marking unhealthy | 3 |
---
### 5. Security Hardening
#### Run as non-root user
```dockerfile
# Debian/Ubuntu based images
RUN addgroup --system app && adduser --system --ingroup app app
USER app
# Alpine based images
RUN addgroup -S app && adduser -S app -G app
USER app
```
#### Use minimal base images
| Base Image | Size | Use Case |
|--------------------|---------|---------------------------------------|
| `alpine` | ~5 MB | General minimal base |
| `*-slim` | ~50 MB | Debian-based with fewer packages |
| `distroless` | ~20 MB | Google's no-shell, no-package-manager |
| `scratch` | 0 MB | Static binaries only (Go, Rust) |
```dockerfile
# Distroless for Python
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
CMD ["main.py"]
```
#### Never put secrets in image layers
```dockerfile
# BAD - secret is baked into image history
COPY .env /app/.env
RUN echo "API_KEY=secret123" >> /app/.env
# GOOD - pass secrets at runtime
CMD ["python", "main.py"]
# docker run -e API_KEY=secret123 myapp
# or docker run --env-file .env myapp
```
#### Multi-stage to exclude build tools
Build tools (compilers, package managers, source code) stay in the builder stage
and never reach the runtime image. This reduces attack surface and image size.
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod
FROM node:20-alpine
WORKDIR /app
# Only the built output and production deps are copied
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]
```
---
### 6. Environment Configuration
#### ARG vs ENV
| Directive | Available at | Persists in image | Use for |
|-----------|-------------|-------------------|-----------------------------|
| `ARG` | Build time | No | Build-time variables |
| `ENV` | Build + run | Yes | Runtime configuration |
```dockerfile
# ARG - only available during build
ARG NODE_ENV=production
ARG BUILD_VERSION=unknown
# ENV - available at build and runtime
ENV NODE_ENV=${NODE_ENV}
ENV APP_VERSION=${BUILD_VERSION}
# Build with: docker build --build-arg BUILD_VERSION=1.2.3 .
```
#### .env files with Compose
```yaml
services:
app:
build: .
# Single .env file
env_file:
- .env
# Multiple files (later files override earlier ones)
env_file:
- .env.defaults
- .env.local
# Inline environment variables (override env_file)
environment:
LOG_LEVEL: debug
DEBUG: "true"
```
#### Secrets management with Docker Compose
```yaml
services:
app:
build: .
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
environment: API_KEY # Read from host environment
```
Inside the container, secrets are mounted at `/run/secrets/<name>` as files.
---
### 7. Networking
#### Bridge networks for service isolation
```yaml
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
networks:
- frontend-net
- backend-net # Can reach the API
api:
build: ./api
ports:
- "8000:8000"
networks:
- backend-net # Reachable by frontend and workers
db:
image: postgres:16-alpine
networks:
- backend-net # Only reachable by api and workers
# No ports exposed to host
worker:
build: ./worker
networks:
- backend-net
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
```
#### Service discovery
Within a Docker Compose network, services reach each other by **service name**
as the hostname.
```python
# In the api service, connect to db using its service name
DATABASE_URL = "postgresql://user:pass@db:5432/app"
# In the frontend service, call the api by service name
API_URL = "http://api:8000"
```
#### Exposing ports
```yaml
services:
app:
ports:
- "3000:3000" # host:container, binds to 0.0.0.0
- "127.0.0.1:3000:3000" # bind to localhost only (more secure)
expose:
- "3000" # expose to other containers only, not host
```
---
## Best Practices
1. **Use multi-stage builds** -- Separate build dependencies from the runtime
image. The final image should contain only what is needed to run the
application.
2. **Pin image tags** -- Use `node:20.11-alpine` or a digest instead of
`node:latest` or `node:20`. Floating tags lead to unpredictable builds.
3. **Order instructions for cache efficiency** -- Copy dependency manifests and
install dependencies before copying application code. This ensures that code
changes do not invalidate the dependency layer cache.
4. **Use .dockerignore** -- Exclude `.git`, `node_modules`, `__pycache__`, `.env`
files, and anything not needed inside the container to keep the build context
small and avoid leaking secrets.
5. **Run as non-root** -- Add a `USER` instruction to run the process as an
unprivileged user. Never run production containers as root.
6. **Combine RUN commands** -- Merge related `RUN` instructions with `&&` to
reduce layers and always clean up apt/apk caches in the same layer that
installs packages.
7. **Use COPY instead of ADD** -- `COPY` is explicit and predictable. `ADD` has
implicit behaviors (tar extraction, URL fetching) that can surprise you.
8. **Set explicit HEALTHCHECK** -- Define health checks in the Dockerfile so
orchestrators know when the container is ready. This prevents routing traffic
to containers that are still starting up.
---
## Common Pitfalls
1. **Bloated images** -- Using full base images like `python:3.12` instead of
`python:3.12-slim` adds hundreds of megabytes. Always prefer slim or alpine
variants. Use multi-stage builds to exclude build tools.
2. **Cache invalidation by COPY order** -- Placing `COPY . .` before
`RUN pip install` means every code change reinstalls all dependencies. Always
copy the dependency manifest first, install, then copy the rest of the code.
3. **Running as root** -- Forgetting the `USER` instruction means the container
process runs as root. If the application is compromised, the attacker has full
control of the container filesystem.
4. **Secrets baked into layers** -- Using `COPY .env .` or `ARG` for secrets
embeds them in the image layer history. Anyone with access to the image can
extract them with `docker history`. Pass secrets at runtime via environment
variables or Docker secrets.
5. **Missing .dockerignore** -- Without a `.dockerignore`, the entire directory
(including `.git`, `node_modules`, `.env` files) is sent as build context.
This slows builds, increases image size, and risks leaking credentials.
6. **Ignoring healthchecks in Compose** -- Using `depends_on` without
`condition: service_healthy` means the dependent service starts as soon as
the database container starts, not when the database is actually ready to
accept connections. Always pair `depends_on` with healthchecks.
---
## Related Skills
- `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
+801
View File
@@ -0,0 +1,801 @@
# DevOps — GitHub Actions Patterns
# GitHub Actions
## When to Use
- Setting up CI/CD pipelines
- Automating tests and builds
- Deployment automation
## When NOT to Use
- GitLab CI projects using `.gitlab-ci.yml` configuration
- Jenkins pipelines using Jenkinsfile or Groovy-based configuration
- CircleCI, Travis CI, or other non-GitHub CI/CD systems
---
## Core Patterns
### 1. CI Pipeline
Complete CI workflow covering checkout, setup, install, lint, test, and build for
both Python and Node.js projects.
#### Node.js CI Pipeline
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
```
#### Python CI Pipeline
```yaml
name: CI - Python
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements-dev.txt
- run: ruff check .
- run: ruff format --check .
- run: mypy src/
test:
name: Test
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements.txt -r requirements-dev.txt
- name: Run tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
run: pytest -v --cov=src --cov-report=xml
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-xml
path: coverage.xml
retention-days: 7
```
---
### 2. Matrix Strategy
Matrix builds run the same job across multiple combinations of OS, language
version, or other variables.
#### OS and version matrix
```yaml
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: "npm"
- run: npm ci
- run: npm test
```
#### Include and exclude
```yaml
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.12"]
exclude:
# Skip Python 3.11 on Windows
- os: windows-latest
python: "3.11"
include:
# Add a specific combination with extra env
- os: ubuntu-latest
python: "3.13"
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental || false }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: pip install -r requirements.txt
- run: pytest
```
---
### 3. Caching
Caching avoids re-downloading dependencies on every run. Use `hashFiles` to
generate cache keys from lockfiles so the cache invalidates when dependencies
change.
#### npm cache
```yaml
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
```
#### pnpm cache
```yaml
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
```
#### pip cache
```yaml
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
pip-${{ runner.os }}-
```
#### Docker layer cache
```yaml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
```
---
### 4. Reusable Workflows
Reusable workflows let you define a workflow once and call it from other
workflows, reducing duplication across repositories.
#### Defining a reusable workflow (`.github/workflows/reusable-test.yml`)
```yaml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
description: "Node.js version to use"
required: false
type: string
default: "20"
working-directory:
description: "Directory to run commands in"
required: false
type: string
default: "."
secrets:
NPM_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
registry-url: "https://registry.npmjs.org"
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm test
```
#### Calling a reusable workflow
```yaml
name: CI
on:
push:
branches: [main]
jobs:
test-app:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
working-directory: "packages/app"
secrets: inherit # Pass all secrets to the called workflow
test-lib:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
working-directory: "packages/lib"
secrets: inherit
```
---
### 5. Composite Actions
Composite actions package multiple steps into a single reusable action. Unlike
reusable workflows, they run inline within the calling job.
#### Action definition (`.github/actions/setup-project/action.yml`)
```yaml
name: "Setup Project"
description: "Install Node.js, enable corepack, and install dependencies"
inputs:
node-version:
description: "Node.js version"
required: false
default: "20"
install-command:
description: "Command to install dependencies"
required: false
default: "pnpm install --frozen-lockfile"
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Enable corepack
shell: bash
run: corepack enable
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: ${{ inputs.install-command }}
```
#### Using the composite action
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
node-version: "20"
- run: pnpm build
```
---
### 6. Deployment
Deployment workflows with environment protection rules, manual approval gates,
and multi-stage promotion.
```yaml
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
permissions:
contents: read
deployments: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable && pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy to staging
env:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
run: |
echo "Deploying to staging..."
# Replace with your actual deploy command
# e.g., aws s3 sync, rsync, wrangler publish, etc.
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
environment:
name: production
url: https://example.com
# Production environment should have required reviewers configured
# in GitHub Settings > Environments > production > Protection rules
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy to production
env:
DEPLOY_TOKEN: ${{ secrets.PRODUCTION_DEPLOY_TOKEN }}
run: |
echo "Deploying to production..."
```
---
### 7. Artifacts
Artifacts let you share data between jobs in the same workflow or persist build
outputs for later download.
#### Upload artifact
```yaml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # Upload even if tests fail
with:
name: test-results-${{ matrix.os }}-${{ matrix.node }}
path: |
test-results/
coverage/
retention-days: 14
if-no-files-found: warn # warn, error, or ignore
```
#### Download artifact in another job
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ls -la dist/
```
#### Download all artifacts
```yaml
- uses: actions/download-artifact@v4
with:
path: all-artifacts/
# Each artifact is placed in a subdirectory named after the artifact
```
---
### 8. Conditional Execution
Control when jobs and steps run using `if` expressions, job dependencies, and
path filters.
#### Path filters on triggers
```yaml
on:
push:
branches: [main]
paths:
- "src/**"
- "package.json"
- "pnpm-lock.yaml"
paths-ignore:
- "docs/**"
- "*.md"
```
#### Conditional jobs
```yaml
jobs:
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'src/api/**'
- 'requirements*.txt'
frontend:
- 'src/web/**'
- 'package.json'
test-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt && pytest
test-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
```
#### Conditional steps with if expressions
```yaml
steps:
- name: Run only on main branch
if: github.ref == 'refs/heads/main'
run: echo "On main"
- name: Run only on pull requests
if: github.event_name == 'pull_request'
run: echo "PR event"
- name: Run only when previous step failed
if: failure()
run: echo "Something failed"
- name: Always run (cleanup)
if: always()
run: echo "Cleanup"
- name: Run only when a label is present
if: contains(github.event.pull_request.labels.*.name, 'deploy')
run: echo "Deploy label found"
- name: Skip for dependabot
if: github.actor != 'dependabot[bot]'
run: npm test
```
#### Job dependencies
```yaml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Linting..."
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
# Runs after both lint and test succeed
deploy:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- run: echo "Deploying..."
# Runs even if test fails, but only after it completes
notify:
runs-on: ubuntu-latest
needs: [test]
if: always()
steps:
- run: echo "Test job status: ${{ needs.test.result }}"
```
---
## Best Practices
1. **Pin action versions with SHA** -- Use the full commit SHA instead of a
mutable tag: `actions/checkout@b4ffde65f...` (or at minimum a major version
tag like `@v4`). This prevents supply-chain attacks where a tag is moved.
2. **Use caching aggressively** -- Cache package manager stores (`~/.npm`,
pnpm store, `~/.cache/pip`) and Docker layers. A well-cached pipeline can
cut run times by 50-80%.
3. **Set minimal permissions** -- Add a top-level `permissions` block and grant
only what is needed. Default permissions are overly broad and pose a security
risk, especially for pull requests from forks.
4. **Run jobs in parallel** -- Structure independent jobs (lint, test, typecheck)
to run concurrently. Use `needs` only when there is a real dependency between
jobs.
5. **Use `fail-fast: false` in matrix builds** -- By default a failing matrix
combination cancels all others. Setting `fail-fast: false` lets all
combinations complete so you get the full picture of what is broken.
6. **Use environment protection rules** -- Configure required reviewers and wait
timers on production environments in GitHub Settings. This adds a human gate
before production deploys.
7. **Extract reusable workflows and composite actions** -- If the same steps
appear in multiple workflows, factor them into a reusable workflow
(`workflow_call`) or composite action to keep things DRY.
8. **Keep secrets out of logs** -- Never `echo` a secret. GitHub masks known
secrets, but dynamically constructed values may leak. Use `::add-mask::` for
runtime values that should be hidden.
---
## Common Pitfalls
1. **Unpinned action versions** -- Using `actions/checkout@main` means your
workflow pulls whatever is on main today. A bad push to that action
repository could break or compromise your builds. Pin to a tag (`@v4`) or
SHA.
2. **Missing caching** -- Running `npm ci` or `pip install` from scratch on
every run wastes minutes. Always configure caching for your package manager,
or use the built-in `cache` option in setup actions (e.g.,
`actions/setup-node` has a `cache` input).
3. **Overly broad triggers** -- Triggering on every push to every branch floods
the queue. Restrict triggers to `main` and pull requests. Use `paths` or
`paths-ignore` to skip runs when only docs or unrelated files change.
4. **Secret exposure in pull requests from forks** -- Secrets are NOT available
in workflows triggered by `pull_request` from forks (by design). If your
workflow needs secrets for fork PRs, use `pull_request_target` carefully and
never check out untrusted code in that context.
5. **Large artifacts without retention limits** -- Uploading artifacts without
setting `retention-days` uses the repository default (90 days), consuming
storage quota. Set short retention for transient artifacts like test results
and coverage reports.
6. **Ignoring `if: always()` for cleanup** -- Steps after a failure are skipped
by default. If you need to upload test results, send notifications, or run
cleanup regardless of prior step results, use `if: always()` or
`if: failure()`.
---
## Related Skills
- `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