@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caching-patterns
|
|
3
|
+
description: Apply caching at every layer — HTTP headers, Redis, stale-while-revalidate, CDN, and in-memory memoization.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Caching Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply caching when you need to reduce latency, lower database load, or decrease
|
|
11
|
+
API costs. Cache at the layer closest to the consumer: browser cache for static
|
|
12
|
+
assets, CDN for public pages, Redis for shared application state, in-memory for
|
|
13
|
+
hot function results. Every cache decision requires answering: how stale is
|
|
14
|
+
acceptable, and how do you invalidate?
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
### 1. HTTP Cache Headers
|
|
19
|
+
|
|
20
|
+
Control browser and CDN caching with response headers.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Static assets (CSS, JS, images) — cache forever, bust with filename hash
|
|
24
|
+
app.use('/assets', express.static('public', {
|
|
25
|
+
maxAge: '1y',
|
|
26
|
+
immutable: true,
|
|
27
|
+
}));
|
|
28
|
+
// Cache-Control: public, max-age=31536000, immutable
|
|
29
|
+
|
|
30
|
+
// API responses — short cache, revalidate
|
|
31
|
+
app.get('/api/products', (req, res) => {
|
|
32
|
+
res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
|
|
33
|
+
res.set('ETag', computeETag(products));
|
|
34
|
+
res.json(products);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Private user data — never cache in shared caches
|
|
38
|
+
app.get('/api/me', authenticate, (req, res) => {
|
|
39
|
+
res.set('Cache-Control', 'private, no-store');
|
|
40
|
+
res.json(req.user);
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Key headers:
|
|
45
|
+
| Header | Purpose |
|
|
46
|
+
|--------|---------|
|
|
47
|
+
| `max-age=N` | Browser caches for N seconds |
|
|
48
|
+
| `s-maxage=N` | CDN/proxy caches for N seconds |
|
|
49
|
+
| `no-store` | Never cache (auth responses, sensitive data) |
|
|
50
|
+
| `stale-while-revalidate=N` | Serve stale while fetching fresh in background |
|
|
51
|
+
| `ETag` + `If-None-Match` | Conditional requests — 304 if unchanged |
|
|
52
|
+
|
|
53
|
+
### 2. Redis Caching
|
|
54
|
+
|
|
55
|
+
Shared cache across multiple server instances.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import Redis from 'ioredis';
|
|
59
|
+
|
|
60
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
61
|
+
|
|
62
|
+
async function cachedQuery<T>(
|
|
63
|
+
key: string,
|
|
64
|
+
ttlSeconds: number,
|
|
65
|
+
fetcher: () => Promise<T>,
|
|
66
|
+
): Promise<T> {
|
|
67
|
+
const cached = await redis.get(key);
|
|
68
|
+
if (cached) return JSON.parse(cached) as T;
|
|
69
|
+
|
|
70
|
+
const fresh = await fetcher();
|
|
71
|
+
await redis.set(key, JSON.stringify(fresh), 'EX', ttlSeconds);
|
|
72
|
+
return fresh;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Usage
|
|
76
|
+
const products = await cachedQuery(
|
|
77
|
+
'products:featured',
|
|
78
|
+
300, // 5 minutes
|
|
79
|
+
() => db.product.findMany({ where: { featured: true } }),
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Stale-While-Revalidate
|
|
84
|
+
|
|
85
|
+
Serve cached data immediately, refresh in the background.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
async function swr<T>(
|
|
89
|
+
key: string,
|
|
90
|
+
ttlSeconds: number,
|
|
91
|
+
staleSeconds: number,
|
|
92
|
+
fetcher: () => Promise<T>,
|
|
93
|
+
): Promise<T> {
|
|
94
|
+
const raw = await redis.get(key);
|
|
95
|
+
|
|
96
|
+
if (raw) {
|
|
97
|
+
const { data, timestamp } = JSON.parse(raw);
|
|
98
|
+
const age = (Date.now() - timestamp) / 1000;
|
|
99
|
+
|
|
100
|
+
if (age < ttlSeconds) {
|
|
101
|
+
return data as T; // fresh
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (age < ttlSeconds + staleSeconds) {
|
|
105
|
+
// Stale — return immediately but refresh in background
|
|
106
|
+
fetcher().then(async (fresh) => {
|
|
107
|
+
await redis.set(key, JSON.stringify({ data: fresh, timestamp: Date.now() }));
|
|
108
|
+
});
|
|
109
|
+
return data as T;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Expired or miss — fetch synchronously
|
|
114
|
+
const fresh = await fetcher();
|
|
115
|
+
await redis.set(key, JSON.stringify({ data: fresh, timestamp: Date.now() }));
|
|
116
|
+
return fresh;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. Cache Invalidation
|
|
121
|
+
|
|
122
|
+
The hardest problem. Three strategies:
|
|
123
|
+
|
|
124
|
+
**Time-based (TTL):** simplest, eventual consistency.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
await redis.set('user:123', JSON.stringify(user), 'EX', 60);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Event-based:** invalidate on mutation.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
async function updateUser(id: string, data: UpdateInput) {
|
|
134
|
+
const user = await db.user.update({ where: { id }, data });
|
|
135
|
+
await redis.del(`user:${id}`); // exact key
|
|
136
|
+
await redis.del('users:list'); // invalidate list
|
|
137
|
+
return user;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Tag-based:** group related keys for bulk invalidation.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
async function setWithTags(key: string, value: string, tags: string[], ttl: number) {
|
|
145
|
+
await redis.set(key, value, 'EX', ttl);
|
|
146
|
+
for (const tag of tags) {
|
|
147
|
+
await redis.sadd(`tag:${tag}`, key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function invalidateTag(tag: string) {
|
|
152
|
+
const keys = await redis.smembers(`tag:${tag}`);
|
|
153
|
+
if (keys.length > 0) {
|
|
154
|
+
await redis.del(...keys);
|
|
155
|
+
}
|
|
156
|
+
await redis.del(`tag:${tag}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Usage
|
|
160
|
+
await setWithTags('product:42', data, ['products', 'featured'], 300);
|
|
161
|
+
await invalidateTag('featured'); // clears all featured product caches
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 5. CDN Caching
|
|
165
|
+
|
|
166
|
+
Use `s-maxage` and `Surrogate-Key` headers for CDN-level control.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
app.get('/blog/:slug', async (req, res) => {
|
|
170
|
+
const post = await getPost(req.params.slug);
|
|
171
|
+
res.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400');
|
|
172
|
+
res.set('Surrogate-Key', `post-${post.id} blog`);
|
|
173
|
+
res.json(post);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Purge on update (Fastly/Cloudflare API)
|
|
177
|
+
async function purgePost(postId: string) {
|
|
178
|
+
await cdnClient.purgeTag(`post-${postId}`);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 6. In-Memory Memoization
|
|
183
|
+
|
|
184
|
+
For expensive pure computations within a single process.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
function memoize<Args extends unknown[], Result>(
|
|
188
|
+
fn: (...args: Args) => Result,
|
|
189
|
+
keyFn: (...args: Args) => string = (...args) => JSON.stringify(args),
|
|
190
|
+
maxSize: number = 1000,
|
|
191
|
+
): (...args: Args) => Result {
|
|
192
|
+
const cache = new Map<string, Result>();
|
|
193
|
+
|
|
194
|
+
return (...args: Args): Result => {
|
|
195
|
+
const key = keyFn(...args);
|
|
196
|
+
if (cache.has(key)) return cache.get(key)!;
|
|
197
|
+
|
|
198
|
+
const result = fn(...args);
|
|
199
|
+
|
|
200
|
+
if (cache.size >= maxSize) {
|
|
201
|
+
const firstKey = cache.keys().next().value!;
|
|
202
|
+
cache.delete(firstKey); // evict oldest
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
cache.set(key, result);
|
|
206
|
+
return result;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const computeScore = memoize(
|
|
211
|
+
(userId: string, period: string) => expensiveCalculation(userId, period),
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 7. Cache Stampede Prevention
|
|
216
|
+
|
|
217
|
+
When cache expires and 100 requests hit the database simultaneously.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const locks = new Map<string, Promise<unknown>>();
|
|
221
|
+
|
|
222
|
+
async function singleFlight<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
|
|
223
|
+
const existing = locks.get(key);
|
|
224
|
+
if (existing) return existing as Promise<T>;
|
|
225
|
+
|
|
226
|
+
const promise = fetcher().finally(() => locks.delete(key));
|
|
227
|
+
locks.set(key, promise);
|
|
228
|
+
return promise;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// All concurrent callers share one fetch
|
|
232
|
+
const data = await singleFlight('expensive-query', () => db.query(...));
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Examples
|
|
236
|
+
|
|
237
|
+
| Layer | TTL | Invalidation | Use case |
|
|
238
|
+
|-------|-----|-------------|----------|
|
|
239
|
+
| Browser | 1 year | Filename hash | Static assets |
|
|
240
|
+
| CDN | 1 hour | Surrogate key purge | Blog posts, marketing pages |
|
|
241
|
+
| Redis | 5 min | Event-based `DEL` | API list endpoints |
|
|
242
|
+
| In-memory | Process lifetime | LRU eviction | Parsed configs, computed scores |
|
|
243
|
+
|
|
244
|
+
## Checklist
|
|
245
|
+
|
|
246
|
+
- [ ] Static assets use long `max-age` with content hashes in filenames
|
|
247
|
+
- [ ] Private data responses include `Cache-Control: private, no-store`
|
|
248
|
+
- [ ] Redis cache uses consistent key naming (`entity:id:subresource`)
|
|
249
|
+
- [ ] Every cache write has an explicit TTL — no indefinite caching
|
|
250
|
+
- [ ] Mutations invalidate related cache keys
|
|
251
|
+
- [ ] SWR pattern is used for data where slight staleness is acceptable
|
|
252
|
+
- [ ] Cache stampede is prevented with single-flight or mutex
|
|
253
|
+
- [ ] In-memory caches have a max size to prevent unbounded growth
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ci-cd
|
|
3
|
+
description: Build reliable CI/CD pipelines with GitHub Actions — caching, matrix builds, deploy gates, and rollbacks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CI/CD Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Set up CI/CD from the first commit. Every merged PR should be automatically
|
|
11
|
+
tested, and every release should flow through a pipeline that catches problems
|
|
12
|
+
before users do. These patterns focus on GitHub Actions but the principles
|
|
13
|
+
apply to any CI system.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### 1. Basic Workflow Structure
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
# .github/workflows/ci.yml
|
|
21
|
+
name: CI
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
push:
|
|
25
|
+
branches: [main]
|
|
26
|
+
pull_request:
|
|
27
|
+
branches: [main]
|
|
28
|
+
|
|
29
|
+
concurrency:
|
|
30
|
+
group: ci-${{ github.ref }}
|
|
31
|
+
cancel-in-progress: true # cancel stale PR runs
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
lint:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: actions/setup-node@v4
|
|
39
|
+
with:
|
|
40
|
+
node-version: 20
|
|
41
|
+
cache: npm
|
|
42
|
+
- run: npm ci
|
|
43
|
+
- run: npm run lint
|
|
44
|
+
|
|
45
|
+
typecheck:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: actions/setup-node@v4
|
|
50
|
+
with: { node-version: 20, cache: npm }
|
|
51
|
+
- run: npm ci
|
|
52
|
+
- run: npx tsc --noEmit
|
|
53
|
+
|
|
54
|
+
test:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
- uses: actions/setup-node@v4
|
|
59
|
+
with: { node-version: 20, cache: npm }
|
|
60
|
+
- run: npm ci
|
|
61
|
+
- run: npm test -- --coverage
|
|
62
|
+
- uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: coverage
|
|
65
|
+
path: coverage/lcov.info
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Matrix Builds
|
|
69
|
+
|
|
70
|
+
Test across multiple Node versions and operating systems.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
test:
|
|
74
|
+
strategy:
|
|
75
|
+
fail-fast: false # don't cancel others when one fails
|
|
76
|
+
matrix:
|
|
77
|
+
node-version: [18, 20, 22]
|
|
78
|
+
os: [ubuntu-latest, macos-latest]
|
|
79
|
+
runs-on: ${{ matrix.os }}
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@v4
|
|
82
|
+
- uses: actions/setup-node@v4
|
|
83
|
+
with:
|
|
84
|
+
node-version: ${{ matrix.node-version }}
|
|
85
|
+
cache: npm
|
|
86
|
+
- run: npm ci
|
|
87
|
+
- run: npm test
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Dependency Caching
|
|
91
|
+
|
|
92
|
+
Cache `node_modules` and build artifacts to cut install time from minutes to
|
|
93
|
+
seconds.
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
- uses: actions/setup-node@v4
|
|
97
|
+
with:
|
|
98
|
+
node-version: 20
|
|
99
|
+
cache: npm # built-in npm cache
|
|
100
|
+
|
|
101
|
+
# For monorepos with turbo
|
|
102
|
+
- uses: actions/cache@v4
|
|
103
|
+
with:
|
|
104
|
+
path: node_modules/.cache/turbo
|
|
105
|
+
key: turbo-${{ runner.os }}-${{ hashFiles('**/turbo.json') }}
|
|
106
|
+
restore-keys: turbo-${{ runner.os }}-
|
|
107
|
+
|
|
108
|
+
# For Docker layer caching
|
|
109
|
+
- uses: docker/build-push-action@v5
|
|
110
|
+
with:
|
|
111
|
+
cache-from: type=gha
|
|
112
|
+
cache-to: type=gha,mode=max
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Deploy Gates
|
|
116
|
+
|
|
117
|
+
Require all checks to pass before deploy. Use environments for approval flows.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
deploy-staging:
|
|
121
|
+
needs: [lint, typecheck, test]
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
environment: staging # auto-deploys
|
|
124
|
+
steps:
|
|
125
|
+
- uses: actions/checkout@v4
|
|
126
|
+
- run: ./scripts/deploy.sh staging
|
|
127
|
+
|
|
128
|
+
deploy-production:
|
|
129
|
+
needs: [deploy-staging]
|
|
130
|
+
runs-on: ubuntu-latest
|
|
131
|
+
environment: production # requires manual approval
|
|
132
|
+
if: github.ref == 'refs/heads/main'
|
|
133
|
+
steps:
|
|
134
|
+
- uses: actions/checkout@v4
|
|
135
|
+
- run: ./scripts/deploy.sh production
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Configure environment protection rules in GitHub:
|
|
139
|
+
- Required reviewers (1-2 people)
|
|
140
|
+
- Wait timer (e.g., 15 minutes after staging deploy)
|
|
141
|
+
- Branch restrictions (only `main`)
|
|
142
|
+
|
|
143
|
+
### 5. Environment Secrets
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
env:
|
|
147
|
+
NODE_ENV: production
|
|
148
|
+
|
|
149
|
+
steps:
|
|
150
|
+
- run: npm run deploy
|
|
151
|
+
env:
|
|
152
|
+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
153
|
+
API_KEY: ${{ secrets.API_KEY }}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Rules:
|
|
157
|
+
- Never echo secrets in logs. GitHub auto-redacts known secrets but custom
|
|
158
|
+
values can leak.
|
|
159
|
+
- Use environment-scoped secrets (`staging` vs `production`)
|
|
160
|
+
- Rotate secrets quarterly. Use OIDC for cloud providers when possible.
|
|
161
|
+
|
|
162
|
+
### 6. Rollback Strategy
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
deploy-production:
|
|
166
|
+
steps:
|
|
167
|
+
- name: Deploy
|
|
168
|
+
id: deploy
|
|
169
|
+
run: ./scripts/deploy.sh production
|
|
170
|
+
continue-on-error: true
|
|
171
|
+
|
|
172
|
+
- name: Smoke test
|
|
173
|
+
id: smoke
|
|
174
|
+
run: ./scripts/smoke-test.sh ${{ vars.PRODUCTION_URL }}
|
|
175
|
+
|
|
176
|
+
- name: Rollback on failure
|
|
177
|
+
if: steps.deploy.outcome == 'failure' || steps.smoke.outcome == 'failure'
|
|
178
|
+
run: ./scripts/rollback.sh production
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Alternative: blue-green deploys where the old version stays running until the
|
|
182
|
+
new one passes health checks.
|
|
183
|
+
|
|
184
|
+
### 7. Reusable Workflows
|
|
185
|
+
|
|
186
|
+
Extract common jobs into shared workflows.
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# .github/workflows/reusable-test.yml
|
|
190
|
+
on:
|
|
191
|
+
workflow_call:
|
|
192
|
+
inputs:
|
|
193
|
+
node-version:
|
|
194
|
+
type: string
|
|
195
|
+
default: '20'
|
|
196
|
+
|
|
197
|
+
jobs:
|
|
198
|
+
test:
|
|
199
|
+
runs-on: ubuntu-latest
|
|
200
|
+
steps:
|
|
201
|
+
- uses: actions/checkout@v4
|
|
202
|
+
- uses: actions/setup-node@v4
|
|
203
|
+
with:
|
|
204
|
+
node-version: ${{ inputs.node-version }}
|
|
205
|
+
cache: npm
|
|
206
|
+
- run: npm ci
|
|
207
|
+
- run: npm test
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
# .github/workflows/ci.yml
|
|
212
|
+
jobs:
|
|
213
|
+
test:
|
|
214
|
+
uses: ./.github/workflows/reusable-test.yml
|
|
215
|
+
with:
|
|
216
|
+
node-version: '20'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 8. PR Annotations
|
|
220
|
+
|
|
221
|
+
Post test results and coverage directly on PRs.
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
- name: Report coverage
|
|
225
|
+
uses: davelosert/vitest-coverage-report-action@v2
|
|
226
|
+
if: github.event_name == 'pull_request'
|
|
227
|
+
with:
|
|
228
|
+
vite-config-path: vitest.config.ts
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Examples
|
|
232
|
+
|
|
233
|
+
| Scenario | Pattern |
|
|
234
|
+
|----------|---------|
|
|
235
|
+
| Flaky tests blocking PRs | `fail-fast: false` + retry step |
|
|
236
|
+
| Slow installs | `actions/setup-node` with `cache: npm` |
|
|
237
|
+
| Multi-platform support | Matrix with `os` and `node-version` |
|
|
238
|
+
| Production safety | Environment protection + smoke tests + rollback |
|
|
239
|
+
| DRY pipeline config | Reusable workflows with `workflow_call` |
|
|
240
|
+
|
|
241
|
+
## Checklist
|
|
242
|
+
|
|
243
|
+
- [ ] CI runs on every push and PR — lint, typecheck, and test in parallel
|
|
244
|
+
- [ ] `concurrency` cancels stale PR runs to save compute
|
|
245
|
+
- [ ] Dependency caching is configured (npm cache at minimum)
|
|
246
|
+
- [ ] Production deploys require staging success + manual approval
|
|
247
|
+
- [ ] Secrets are scoped to environments, not repository-wide
|
|
248
|
+
- [ ] Deploy step includes smoke test with automatic rollback on failure
|
|
249
|
+
- [ ] Matrix builds cover minimum and latest supported Node versions
|
|
250
|
+
- [ ] Reusable workflows extract duplicated job definitions
|
|
251
|
+
- [ ] No secrets are printed in logs (audit with `ACTIONS_STEP_DEBUG`)
|