autoworkflow 3.1.5 → 3.6.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# GitHub Actions Skill
|
|
2
|
+
|
|
3
|
+
## Complete CI/CD Workflow
|
|
4
|
+
\`\`\`yaml
|
|
5
|
+
name: CI/CD
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main, develop]
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [main]
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
NODE_VERSION: 20
|
|
15
|
+
REGISTRY: ghcr.io
|
|
16
|
+
IMAGE_NAME: \${{ github.repository }}
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
lint:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: \${{ env.NODE_VERSION }}
|
|
26
|
+
cache: 'npm'
|
|
27
|
+
- run: npm ci
|
|
28
|
+
- run: npm run lint
|
|
29
|
+
- run: npm run typecheck
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
needs: lint
|
|
34
|
+
services:
|
|
35
|
+
postgres:
|
|
36
|
+
image: postgres:16
|
|
37
|
+
env:
|
|
38
|
+
POSTGRES_PASSWORD: postgres
|
|
39
|
+
POSTGRES_DB: test
|
|
40
|
+
ports:
|
|
41
|
+
- 5432:5432
|
|
42
|
+
options: >-
|
|
43
|
+
--health-cmd pg_isready
|
|
44
|
+
--health-interval 10s
|
|
45
|
+
--health-timeout 5s
|
|
46
|
+
--health-retries 5
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: actions/setup-node@v4
|
|
50
|
+
with:
|
|
51
|
+
node-version: \${{ env.NODE_VERSION }}
|
|
52
|
+
cache: 'npm'
|
|
53
|
+
- run: npm ci
|
|
54
|
+
- run: npm test -- --coverage
|
|
55
|
+
env:
|
|
56
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
|
|
57
|
+
- uses: codecov/codecov-action@v4
|
|
58
|
+
with:
|
|
59
|
+
token: \${{ secrets.CODECOV_TOKEN }}
|
|
60
|
+
|
|
61
|
+
build:
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
needs: test
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
- uses: actions/setup-node@v4
|
|
67
|
+
with:
|
|
68
|
+
node-version: \${{ env.NODE_VERSION }}
|
|
69
|
+
cache: 'npm'
|
|
70
|
+
- run: npm ci
|
|
71
|
+
- run: npm run build
|
|
72
|
+
- uses: actions/upload-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: build
|
|
75
|
+
path: dist/
|
|
76
|
+
retention-days: 7
|
|
77
|
+
|
|
78
|
+
deploy:
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
needs: build
|
|
81
|
+
if: github.ref == 'refs/heads/main'
|
|
82
|
+
environment: production
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
- uses: actions/download-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
name: build
|
|
88
|
+
path: dist/
|
|
89
|
+
- name: Deploy to Vercel
|
|
90
|
+
uses: amondnet/vercel-action@v25
|
|
91
|
+
with:
|
|
92
|
+
vercel-token: \${{ secrets.VERCEL_TOKEN }}
|
|
93
|
+
vercel-org-id: \${{ secrets.VERCEL_ORG_ID }}
|
|
94
|
+
vercel-project-id: \${{ secrets.VERCEL_PROJECT_ID }}
|
|
95
|
+
vercel-args: '--prod'
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## Matrix Testing
|
|
99
|
+
\`\`\`yaml
|
|
100
|
+
jobs:
|
|
101
|
+
test:
|
|
102
|
+
runs-on: \${{ matrix.os }}
|
|
103
|
+
strategy:
|
|
104
|
+
fail-fast: false
|
|
105
|
+
matrix:
|
|
106
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
107
|
+
node-version: [18, 20, 22]
|
|
108
|
+
exclude:
|
|
109
|
+
- os: windows-latest
|
|
110
|
+
node-version: 18
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
113
|
+
- uses: actions/setup-node@v4
|
|
114
|
+
with:
|
|
115
|
+
node-version: \${{ matrix.node-version }}
|
|
116
|
+
cache: 'npm'
|
|
117
|
+
- run: npm ci
|
|
118
|
+
- run: npm test
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
## Caching Strategies
|
|
122
|
+
\`\`\`yaml
|
|
123
|
+
# npm cache (built into setup-node)
|
|
124
|
+
- uses: actions/setup-node@v4
|
|
125
|
+
with:
|
|
126
|
+
node-version: 20
|
|
127
|
+
cache: 'npm'
|
|
128
|
+
|
|
129
|
+
# pnpm cache
|
|
130
|
+
- uses: pnpm/action-setup@v3
|
|
131
|
+
with:
|
|
132
|
+
version: 9
|
|
133
|
+
- uses: actions/setup-node@v4
|
|
134
|
+
with:
|
|
135
|
+
node-version: 20
|
|
136
|
+
cache: 'pnpm'
|
|
137
|
+
|
|
138
|
+
# Custom caching
|
|
139
|
+
- uses: actions/cache@v4
|
|
140
|
+
with:
|
|
141
|
+
path: |
|
|
142
|
+
~/.npm
|
|
143
|
+
node_modules
|
|
144
|
+
.next/cache
|
|
145
|
+
key: \${{ runner.os }}-node-\${{ hashFiles('**/package-lock.json') }}
|
|
146
|
+
restore-keys: |
|
|
147
|
+
\${{ runner.os }}-node-
|
|
148
|
+
|
|
149
|
+
# Turborepo remote cache
|
|
150
|
+
- name: Turbo Cache
|
|
151
|
+
uses: actions/cache@v4
|
|
152
|
+
with:
|
|
153
|
+
path: .turbo
|
|
154
|
+
key: \${{ runner.os }}-turbo-\${{ github.sha }}
|
|
155
|
+
restore-keys: |
|
|
156
|
+
\${{ runner.os }}-turbo-
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
## Docker Build & Push
|
|
160
|
+
\`\`\`yaml
|
|
161
|
+
jobs:
|
|
162
|
+
docker:
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
permissions:
|
|
165
|
+
contents: read
|
|
166
|
+
packages: write
|
|
167
|
+
steps:
|
|
168
|
+
- uses: actions/checkout@v4
|
|
169
|
+
|
|
170
|
+
- name: Set up Docker Buildx
|
|
171
|
+
uses: docker/setup-buildx-action@v3
|
|
172
|
+
|
|
173
|
+
- name: Login to Container Registry
|
|
174
|
+
uses: docker/login-action@v3
|
|
175
|
+
with:
|
|
176
|
+
registry: \${{ env.REGISTRY }}
|
|
177
|
+
username: \${{ github.actor }}
|
|
178
|
+
password: \${{ secrets.GITHUB_TOKEN }}
|
|
179
|
+
|
|
180
|
+
- name: Extract metadata
|
|
181
|
+
id: meta
|
|
182
|
+
uses: docker/metadata-action@v5
|
|
183
|
+
with:
|
|
184
|
+
images: \${{ env.REGISTRY }}/\${{ env.IMAGE_NAME }}
|
|
185
|
+
tags: |
|
|
186
|
+
type=sha
|
|
187
|
+
type=ref,event=branch
|
|
188
|
+
type=semver,pattern={{version}}
|
|
189
|
+
|
|
190
|
+
- name: Build and push
|
|
191
|
+
uses: docker/build-push-action@v5
|
|
192
|
+
with:
|
|
193
|
+
context: .
|
|
194
|
+
push: true
|
|
195
|
+
tags: \${{ steps.meta.outputs.tags }}
|
|
196
|
+
labels: \${{ steps.meta.outputs.labels }}
|
|
197
|
+
cache-from: type=gha
|
|
198
|
+
cache-to: type=gha,mode=max
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
## Reusable Workflow
|
|
202
|
+
\`\`\`yaml
|
|
203
|
+
# .github/workflows/reusable-test.yml
|
|
204
|
+
name: Reusable Test
|
|
205
|
+
|
|
206
|
+
on:
|
|
207
|
+
workflow_call:
|
|
208
|
+
inputs:
|
|
209
|
+
node-version:
|
|
210
|
+
required: false
|
|
211
|
+
type: string
|
|
212
|
+
default: '20'
|
|
213
|
+
secrets:
|
|
214
|
+
NPM_TOKEN:
|
|
215
|
+
required: false
|
|
216
|
+
|
|
217
|
+
jobs:
|
|
218
|
+
test:
|
|
219
|
+
runs-on: ubuntu-latest
|
|
220
|
+
steps:
|
|
221
|
+
- uses: actions/checkout@v4
|
|
222
|
+
- uses: actions/setup-node@v4
|
|
223
|
+
with:
|
|
224
|
+
node-version: \${{ inputs.node-version }}
|
|
225
|
+
- run: npm ci
|
|
226
|
+
- run: npm test
|
|
227
|
+
|
|
228
|
+
# Usage in another workflow
|
|
229
|
+
jobs:
|
|
230
|
+
call-tests:
|
|
231
|
+
uses: ./.github/workflows/reusable-test.yml
|
|
232
|
+
with:
|
|
233
|
+
node-version: '20'
|
|
234
|
+
secrets:
|
|
235
|
+
NPM_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
## Release Workflow
|
|
239
|
+
\`\`\`yaml
|
|
240
|
+
name: Release
|
|
241
|
+
|
|
242
|
+
on:
|
|
243
|
+
push:
|
|
244
|
+
tags:
|
|
245
|
+
- 'v*'
|
|
246
|
+
|
|
247
|
+
jobs:
|
|
248
|
+
release:
|
|
249
|
+
runs-on: ubuntu-latest
|
|
250
|
+
permissions:
|
|
251
|
+
contents: write
|
|
252
|
+
steps:
|
|
253
|
+
- uses: actions/checkout@v4
|
|
254
|
+
with:
|
|
255
|
+
fetch-depth: 0
|
|
256
|
+
|
|
257
|
+
- name: Generate changelog
|
|
258
|
+
id: changelog
|
|
259
|
+
uses: orhun/git-cliff-action@v3
|
|
260
|
+
with:
|
|
261
|
+
args: --latest
|
|
262
|
+
|
|
263
|
+
- name: Create Release
|
|
264
|
+
uses: softprops/action-gh-release@v2
|
|
265
|
+
with:
|
|
266
|
+
body: \${{ steps.changelog.outputs.content }}
|
|
267
|
+
draft: false
|
|
268
|
+
prerelease: \${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') }}
|
|
269
|
+
\`\`\`
|
|
270
|
+
|
|
271
|
+
## Scheduled Jobs
|
|
272
|
+
\`\`\`yaml
|
|
273
|
+
name: Scheduled Tasks
|
|
274
|
+
|
|
275
|
+
on:
|
|
276
|
+
schedule:
|
|
277
|
+
- cron: '0 0 * * *' # Daily at midnight UTC
|
|
278
|
+
workflow_dispatch: # Allow manual trigger
|
|
279
|
+
|
|
280
|
+
jobs:
|
|
281
|
+
dependency-check:
|
|
282
|
+
runs-on: ubuntu-latest
|
|
283
|
+
steps:
|
|
284
|
+
- uses: actions/checkout@v4
|
|
285
|
+
- run: npm audit --audit-level=high
|
|
286
|
+
- run: npx npm-check-updates -u
|
|
287
|
+
continue-on-error: true
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
## Environment & Secrets
|
|
291
|
+
\`\`\`yaml
|
|
292
|
+
jobs:
|
|
293
|
+
deploy:
|
|
294
|
+
runs-on: ubuntu-latest
|
|
295
|
+
environment:
|
|
296
|
+
name: production
|
|
297
|
+
url: https://myapp.com
|
|
298
|
+
steps:
|
|
299
|
+
- name: Deploy
|
|
300
|
+
env:
|
|
301
|
+
# Repository secrets
|
|
302
|
+
API_KEY: \${{ secrets.API_KEY }}
|
|
303
|
+
# Environment secrets
|
|
304
|
+
DATABASE_URL: \${{ secrets.DATABASE_URL }}
|
|
305
|
+
# GitHub token (auto-provided)
|
|
306
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
307
|
+
run: ./deploy.sh
|
|
308
|
+
\`\`\`
|
|
309
|
+
|
|
310
|
+
## Conditional Execution
|
|
311
|
+
\`\`\`yaml
|
|
312
|
+
jobs:
|
|
313
|
+
deploy:
|
|
314
|
+
if: github.ref == 'refs/heads/main'
|
|
315
|
+
|
|
316
|
+
deploy-preview:
|
|
317
|
+
if: github.event_name == 'pull_request'
|
|
318
|
+
|
|
319
|
+
skip-ci:
|
|
320
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
321
|
+
|
|
322
|
+
only-docs:
|
|
323
|
+
if: contains(github.event.head_commit.modified, 'docs/')
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
## Common Actions Reference
|
|
327
|
+
\`\`\`yaml
|
|
328
|
+
actions/checkout@v4 # Clone repository
|
|
329
|
+
actions/setup-node@v4 # Setup Node.js
|
|
330
|
+
actions/cache@v4 # Cache dependencies
|
|
331
|
+
actions/upload-artifact@v4 # Upload build artifacts
|
|
332
|
+
actions/download-artifact@v4
|
|
333
|
+
docker/build-push-action@v5
|
|
334
|
+
aws-actions/configure-aws-credentials@v4
|
|
335
|
+
azure/webapps-deploy@v3
|
|
336
|
+
google-github-actions/auth@v2
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
## ❌ DON'T
|
|
340
|
+
- Commit secrets to workflow files
|
|
341
|
+
- Use \`pull_request_target\` without understanding security
|
|
342
|
+
- Skip checkout before using repo files
|
|
343
|
+
- Ignore failing steps with \`continue-on-error\` carelessly
|
|
344
|
+
- Use deprecated action versions
|
|
345
|
+
|
|
346
|
+
## ✅ DO
|
|
347
|
+
- Pin action versions (use SHA for critical actions)
|
|
348
|
+
- Use job dependencies with \`needs\`
|
|
349
|
+
- Cache dependencies for faster builds
|
|
350
|
+
- Use matrix for cross-platform/version testing
|
|
351
|
+
- Set appropriate permissions
|
|
352
|
+
- Use environments for deployments
|
|
353
|
+
- Add workflow_dispatch for manual runs
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Go Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
myapp/
|
|
6
|
+
├── cmd/
|
|
7
|
+
│ └── api/
|
|
8
|
+
│ └── main.go # Entry point
|
|
9
|
+
├── internal/
|
|
10
|
+
│ ├── config/ # Configuration
|
|
11
|
+
│ ├── handler/ # HTTP handlers
|
|
12
|
+
│ ├── middleware/ # HTTP middleware
|
|
13
|
+
│ ├── model/ # Domain models
|
|
14
|
+
│ ├── repository/ # Data access
|
|
15
|
+
│ └── service/ # Business logic
|
|
16
|
+
├── pkg/ # Public packages
|
|
17
|
+
├── go.mod
|
|
18
|
+
└── go.sum
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
## Structs and Methods
|
|
22
|
+
\`\`\`go
|
|
23
|
+
package model
|
|
24
|
+
|
|
25
|
+
import (
|
|
26
|
+
"time"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
type User struct {
|
|
30
|
+
ID string \`json:"id"\`
|
|
31
|
+
Email string \`json:"email"\`
|
|
32
|
+
Name string \`json:"name"\`
|
|
33
|
+
CreatedAt time.Time \`json:"created_at"\`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// NewUser creates a User with generated ID and timestamp
|
|
37
|
+
func NewUser(email, name string) *User {
|
|
38
|
+
return &User{
|
|
39
|
+
ID: generateID(),
|
|
40
|
+
Email: email,
|
|
41
|
+
Name: name,
|
|
42
|
+
CreatedAt: time.Now(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate checks if user data is valid
|
|
47
|
+
func (u *User) Validate() error {
|
|
48
|
+
if u.Email == "" {
|
|
49
|
+
return errors.New("email is required")
|
|
50
|
+
}
|
|
51
|
+
if !strings.Contains(u.Email, "@") {
|
|
52
|
+
return errors.New("invalid email format")
|
|
53
|
+
}
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Interfaces and Dependency Injection
|
|
59
|
+
\`\`\`go
|
|
60
|
+
package repository
|
|
61
|
+
|
|
62
|
+
import "context"
|
|
63
|
+
|
|
64
|
+
// UserRepository defines the contract for user data access
|
|
65
|
+
type UserRepository interface {
|
|
66
|
+
GetByID(ctx context.Context, id string) (*model.User, error)
|
|
67
|
+
GetByEmail(ctx context.Context, email string) (*model.User, error)
|
|
68
|
+
Create(ctx context.Context, user *model.User) error
|
|
69
|
+
Update(ctx context.Context, user *model.User) error
|
|
70
|
+
Delete(ctx context.Context, id string) error
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// PostgresUserRepository implements UserRepository with PostgreSQL
|
|
74
|
+
type PostgresUserRepository struct {
|
|
75
|
+
db *sql.DB
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
|
|
79
|
+
return &PostgresUserRepository{db: db}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (r *PostgresUserRepository) GetByID(ctx context.Context, id string) (*model.User, error) {
|
|
83
|
+
var user model.User
|
|
84
|
+
err := r.db.QueryRowContext(ctx,
|
|
85
|
+
"SELECT id, email, name, created_at FROM users WHERE id = $1",
|
|
86
|
+
id,
|
|
87
|
+
).Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt)
|
|
88
|
+
|
|
89
|
+
if err == sql.ErrNoRows {
|
|
90
|
+
return nil, ErrNotFound
|
|
91
|
+
}
|
|
92
|
+
if err != nil {
|
|
93
|
+
return nil, fmt.Errorf("query user: %w", err)
|
|
94
|
+
}
|
|
95
|
+
return &user, nil
|
|
96
|
+
}
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
## Error Handling
|
|
100
|
+
\`\`\`go
|
|
101
|
+
package apperror
|
|
102
|
+
|
|
103
|
+
import (
|
|
104
|
+
"errors"
|
|
105
|
+
"fmt"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// Sentinel errors
|
|
109
|
+
var (
|
|
110
|
+
ErrNotFound = errors.New("not found")
|
|
111
|
+
ErrUnauthorized = errors.New("unauthorized")
|
|
112
|
+
ErrForbidden = errors.New("forbidden")
|
|
113
|
+
ErrConflict = errors.New("resource already exists")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// AppError wraps errors with additional context
|
|
117
|
+
type AppError struct {
|
|
118
|
+
Err error
|
|
119
|
+
Message string
|
|
120
|
+
Code string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (e *AppError) Error() string {
|
|
124
|
+
return e.Message
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func (e *AppError) Unwrap() error {
|
|
128
|
+
return e.Err
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Wrap creates an AppError with context
|
|
132
|
+
func Wrap(err error, message string) *AppError {
|
|
133
|
+
return &AppError{
|
|
134
|
+
Err: err,
|
|
135
|
+
Message: message,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Usage with error wrapping
|
|
140
|
+
func GetUser(ctx context.Context, id string) (*User, error) {
|
|
141
|
+
user, err := repo.GetByID(ctx, id)
|
|
142
|
+
if err != nil {
|
|
143
|
+
if errors.Is(err, ErrNotFound) {
|
|
144
|
+
return nil, ErrNotFound // Propagate sentinel error
|
|
145
|
+
}
|
|
146
|
+
return nil, fmt.Errorf("get user %s: %w", id, err) // Wrap with context
|
|
147
|
+
}
|
|
148
|
+
return user, nil
|
|
149
|
+
}
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
## Context and Cancellation
|
|
153
|
+
\`\`\`go
|
|
154
|
+
package main
|
|
155
|
+
|
|
156
|
+
import (
|
|
157
|
+
"context"
|
|
158
|
+
"time"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
func processRequest(ctx context.Context) error {
|
|
162
|
+
// Create timeout context
|
|
163
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
164
|
+
defer cancel() // Always call cancel to release resources
|
|
165
|
+
|
|
166
|
+
// Pass context to operations
|
|
167
|
+
result, err := fetchData(ctx)
|
|
168
|
+
if err != nil {
|
|
169
|
+
return err
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for cancellation in long operations
|
|
173
|
+
select {
|
|
174
|
+
case <-ctx.Done():
|
|
175
|
+
return ctx.Err()
|
|
176
|
+
default:
|
|
177
|
+
// Continue processing
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return process(ctx, result)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Context with values (use sparingly, prefer explicit parameters)
|
|
184
|
+
type contextKey string
|
|
185
|
+
|
|
186
|
+
const userIDKey contextKey = "userID"
|
|
187
|
+
|
|
188
|
+
func WithUserID(ctx context.Context, userID string) context.Context {
|
|
189
|
+
return context.WithValue(ctx, userIDKey, userID)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func UserIDFromContext(ctx context.Context) (string, bool) {
|
|
193
|
+
userID, ok := ctx.Value(userIDKey).(string)
|
|
194
|
+
return userID, ok
|
|
195
|
+
}
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
## Goroutines and Channels
|
|
199
|
+
\`\`\`go
|
|
200
|
+
package main
|
|
201
|
+
|
|
202
|
+
import (
|
|
203
|
+
"context"
|
|
204
|
+
"sync"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Worker pool pattern
|
|
208
|
+
func processItems(ctx context.Context, items []Item) error {
|
|
209
|
+
numWorkers := 5
|
|
210
|
+
jobs := make(chan Item, len(items))
|
|
211
|
+
results := make(chan Result, len(items))
|
|
212
|
+
errs := make(chan error, numWorkers)
|
|
213
|
+
|
|
214
|
+
// Start workers
|
|
215
|
+
var wg sync.WaitGroup
|
|
216
|
+
for i := 0; i < numWorkers; i++ {
|
|
217
|
+
wg.Add(1)
|
|
218
|
+
go func() {
|
|
219
|
+
defer wg.Done()
|
|
220
|
+
for item := range jobs {
|
|
221
|
+
select {
|
|
222
|
+
case <-ctx.Done():
|
|
223
|
+
return
|
|
224
|
+
default:
|
|
225
|
+
result, err := processItem(ctx, item)
|
|
226
|
+
if err != nil {
|
|
227
|
+
errs <- err
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
results <- result
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Send jobs
|
|
237
|
+
for _, item := range items {
|
|
238
|
+
jobs <- item
|
|
239
|
+
}
|
|
240
|
+
close(jobs)
|
|
241
|
+
|
|
242
|
+
// Wait for completion
|
|
243
|
+
go func() {
|
|
244
|
+
wg.Wait()
|
|
245
|
+
close(results)
|
|
246
|
+
close(errs)
|
|
247
|
+
}()
|
|
248
|
+
|
|
249
|
+
// Collect results
|
|
250
|
+
var allResults []Result
|
|
251
|
+
for result := range results {
|
|
252
|
+
allResults = append(allResults, result)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check for errors
|
|
256
|
+
if err := <-errs; err != nil {
|
|
257
|
+
return err
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return nil
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// errgroup for simpler error handling
|
|
264
|
+
import "golang.org/x/sync/errgroup"
|
|
265
|
+
|
|
266
|
+
func fetchAll(ctx context.Context, urls []string) ([]Response, error) {
|
|
267
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
268
|
+
responses := make([]Response, len(urls))
|
|
269
|
+
|
|
270
|
+
for i, url := range urls {
|
|
271
|
+
i, url := i, url // Capture loop variables
|
|
272
|
+
g.Go(func() error {
|
|
273
|
+
resp, err := fetch(ctx, url)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return err
|
|
276
|
+
}
|
|
277
|
+
responses[i] = resp
|
|
278
|
+
return nil
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if err := g.Wait(); err != nil {
|
|
283
|
+
return nil, err
|
|
284
|
+
}
|
|
285
|
+
return responses, nil
|
|
286
|
+
}
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
## Testing
|
|
290
|
+
\`\`\`go
|
|
291
|
+
package service_test
|
|
292
|
+
|
|
293
|
+
import (
|
|
294
|
+
"context"
|
|
295
|
+
"testing"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// Table-driven tests
|
|
299
|
+
func TestValidateEmail(t *testing.T) {
|
|
300
|
+
tests := []struct {
|
|
301
|
+
name string
|
|
302
|
+
email string
|
|
303
|
+
wantErr bool
|
|
304
|
+
}{
|
|
305
|
+
{"valid email", "user@example.com", false},
|
|
306
|
+
{"empty email", "", true},
|
|
307
|
+
{"missing @", "invalid-email", true},
|
|
308
|
+
{"missing domain", "user@", true},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for _, tt := range tests {
|
|
312
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
313
|
+
err := ValidateEmail(tt.email)
|
|
314
|
+
if (err != nil) != tt.wantErr {
|
|
315
|
+
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Testing with mocks
|
|
322
|
+
type mockUserRepository struct {
|
|
323
|
+
users map[string]*model.User
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func (m *mockUserRepository) GetByID(ctx context.Context, id string) (*model.User, error) {
|
|
327
|
+
user, ok := m.users[id]
|
|
328
|
+
if !ok {
|
|
329
|
+
return nil, ErrNotFound
|
|
330
|
+
}
|
|
331
|
+
return user, nil
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func TestUserService_GetUser(t *testing.T) {
|
|
335
|
+
repo := &mockUserRepository{
|
|
336
|
+
users: map[string]*model.User{
|
|
337
|
+
"123": {ID: "123", Email: "test@example.com"},
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
svc := NewUserService(repo)
|
|
341
|
+
|
|
342
|
+
t.Run("existing user", func(t *testing.T) {
|
|
343
|
+
user, err := svc.GetUser(context.Background(), "123")
|
|
344
|
+
if err != nil {
|
|
345
|
+
t.Fatalf("unexpected error: %v", err)
|
|
346
|
+
}
|
|
347
|
+
if user.Email != "test@example.com" {
|
|
348
|
+
t.Errorf("got email %q, want %q", user.Email, "test@example.com")
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
t.Run("non-existing user", func(t *testing.T) {
|
|
353
|
+
_, err := svc.GetUser(context.Background(), "999")
|
|
354
|
+
if !errors.Is(err, ErrNotFound) {
|
|
355
|
+
t.Errorf("got error %v, want ErrNotFound", err)
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
\`\`\`
|
|
360
|
+
|
|
361
|
+
## ✅ DO
|
|
362
|
+
- Handle all errors explicitly - never ignore with \`_\`
|
|
363
|
+
- Use \`context.Context\` as first parameter
|
|
364
|
+
- Wrap errors with \`fmt.Errorf("context: %w", err)\`
|
|
365
|
+
- Use interfaces for dependency injection
|
|
366
|
+
- Prefer composition over inheritance
|
|
367
|
+
- Use \`defer\` for cleanup (close files, unlock mutexes)
|
|
368
|
+
- Run \`go vet\` and \`golangci-lint\`
|
|
369
|
+
- Use table-driven tests
|
|
370
|
+
|
|
371
|
+
## ❌ DON'T
|
|
372
|
+
- Don't use \`panic\` for error handling (only for programming errors)
|
|
373
|
+
- Don't pass context in structs, pass as function parameter
|
|
374
|
+
- Don't use \`context.Background()\` in request handlers (use request context)
|
|
375
|
+
- Don't ignore goroutine leaks - always ensure goroutines can exit
|
|
376
|
+
- Don't use naked returns in long functions
|
|
377
|
+
- Don't use \`init()\` for complex initialization
|