mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-14 06:04:57 +03:00
feat: improved the Claude Kit as a plugin
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user