create-gef 1.0.0 → 1.1.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/.gef/ENGINEERING_PLAYBOOK.md +141 -0
- package/.gef/prompts/adr_writing.md +50 -0
- package/.gef/prompts/bugfix.md +31 -0
- package/.gef/prompts/code_review.md +40 -0
- package/.gef/prompts/feature_development.md +37 -0
- package/.gef/prompts/new_project_kickoff.md +41 -0
- package/.gef/prompts/system_prompt.md +41 -0
- package/.github/workflows/release-please.yml +19 -0
- package/CHANGELOG.md +63 -0
- package/ENGINEERING_PLAYBOOK.md +88 -341
- package/PROJECT_CONFIG.template.md +15 -28
- package/README.md +87 -20
- package/generator/cli/help.js +65 -0
- package/generator/cli/questions.js +98 -0
- package/generator/features/scaffold-ci.js +441 -0
- package/generator/features/scaffold-docker.js +159 -0
- package/generator/features/scaffold-gef.js +158 -0
- package/generator/features/scaffold-git.js +138 -0
- package/generator/features/scaffold-linter.js +91 -0
- package/generator/features/scaffold-stack.js +101 -0
- package/generator/features/update.js +61 -0
- package/generator/index.js +37 -668
- package/generator/templates/adr-template.md +22 -0
- package/hooks/commit-msg +4 -3
- package/hooks/pre-commit +2 -2
- package/package.json +1 -1
- package/prompts/adr_writing.md +45 -9
- package/prompts/bugfix.md +24 -8
- package/prompts/code_review.md +34 -8
- package/prompts/feature_development.md +10 -1
- package/prompts/new_project_kickoff.md +33 -6
- package/prompts/system_prompt.md +30 -13
- package/website/.oxlintrc.json +8 -0
- package/website/README.md +16 -0
- package/website/index.html +13 -0
- package/website/package-lock.json +1372 -0
- package/website/package.json +25 -0
- package/website/public/favicon.svg +1 -0
- package/website/public/icons.svg +24 -0
- package/website/src/App.css +1 -0
- package/website/src/App.tsx +167 -0
- package/website/src/assets/hero.png +0 -0
- package/website/src/assets/react.svg +1 -0
- package/website/src/assets/vite.svg +1 -0
- package/website/src/components/FeatureCard.tsx +28 -0
- package/website/src/components/TerminalDemo.tsx +84 -0
- package/website/src/index.css +152 -0
- package/website/src/main.tsx +10 -0
- package/website/tsconfig.app.json +26 -0
- package/website/tsconfig.json +7 -0
- package/website/tsconfig.node.json +23 -0
- package/website/vite.config.ts +7 -0
- package/hooks/pre-push +0 -25
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// features/scaffold-ci.js — Pipeline CI/CD complet et progressif selon la sévérité
|
|
2
|
+
// Réf. Playbook §1 : SRP. §4 : OWASP. §6 : CI/CD
|
|
3
|
+
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────
|
|
8
|
+
// BLOC : Trigger / On
|
|
9
|
+
// ─────────────────────────────────────────────
|
|
10
|
+
function buildTriggers(gitWorkflow) {
|
|
11
|
+
const branchFilter = gitWorkflow?.includes('Trunk')
|
|
12
|
+
? ` branches:\n - main`
|
|
13
|
+
: ` branches:\n - main\n - 'feat/**'\n - 'fix/**'`;
|
|
14
|
+
|
|
15
|
+
return `on:
|
|
16
|
+
push:
|
|
17
|
+
${branchFilter}
|
|
18
|
+
tags:
|
|
19
|
+
- 'v*.*.*'
|
|
20
|
+
pull_request:
|
|
21
|
+
branches:
|
|
22
|
+
- main`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────
|
|
26
|
+
// BLOC : Services DB éphémères pour les tests
|
|
27
|
+
// ─────────────────────────────────────────────
|
|
28
|
+
function buildServiceBlock(database) {
|
|
29
|
+
if (database === 'PostgreSQL') {
|
|
30
|
+
return ` services:
|
|
31
|
+
postgres:
|
|
32
|
+
image: postgres:16-alpine
|
|
33
|
+
env:
|
|
34
|
+
POSTGRES_USER: test
|
|
35
|
+
POSTGRES_PASSWORD: test
|
|
36
|
+
POSTGRES_DB: test_db
|
|
37
|
+
ports:
|
|
38
|
+
- 5432:5432
|
|
39
|
+
options: >-
|
|
40
|
+
--health-cmd pg_isready
|
|
41
|
+
--health-interval 10s
|
|
42
|
+
--health-timeout 5s
|
|
43
|
+
--health-retries 5`;
|
|
44
|
+
}
|
|
45
|
+
if (database === 'MongoDB') {
|
|
46
|
+
return ` services:
|
|
47
|
+
mongo:
|
|
48
|
+
image: mongo:7
|
|
49
|
+
ports:
|
|
50
|
+
- 27017:27017
|
|
51
|
+
options: >-
|
|
52
|
+
--health-cmd "mongosh --eval 'db.runCommand({ ping: 1 })'"
|
|
53
|
+
--health-interval 10s
|
|
54
|
+
--health-timeout 5s
|
|
55
|
+
--health-retries 5`;
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────
|
|
61
|
+
// BLOC : Setup runtime
|
|
62
|
+
// ─────────────────────────────────────────────
|
|
63
|
+
function buildRuntimeBlock(isNode, isPython) {
|
|
64
|
+
if (isNode) {
|
|
65
|
+
return ` - name: Setup Node.js
|
|
66
|
+
uses: actions/setup-node@v4
|
|
67
|
+
with:
|
|
68
|
+
node-version: '20'
|
|
69
|
+
cache: 'npm'
|
|
70
|
+
|
|
71
|
+
- name: Installer les dépendances
|
|
72
|
+
run: npm ci`;
|
|
73
|
+
}
|
|
74
|
+
if (isPython) {
|
|
75
|
+
return ` - name: Setup Python
|
|
76
|
+
uses: actions/setup-python@v5
|
|
77
|
+
with:
|
|
78
|
+
python-version: '3.12'
|
|
79
|
+
cache: 'pip'
|
|
80
|
+
|
|
81
|
+
- name: Installer les dépendances
|
|
82
|
+
run: pip install -r requirements.txt`;
|
|
83
|
+
}
|
|
84
|
+
return ` - name: Setup\n run: echo "Configurez votre runtime ici"`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─────────────────────────────────────────────
|
|
88
|
+
// BLOC : Lint
|
|
89
|
+
// ─────────────────────────────────────────────
|
|
90
|
+
function buildLintBlock(isNode, isPython, linter) {
|
|
91
|
+
if (isNode) {
|
|
92
|
+
const cmd = linter?.includes('Biome') ? 'npx biome check .' : 'npm run lint --if-present';
|
|
93
|
+
return ` - name: Lint\n run: ${cmd}`;
|
|
94
|
+
}
|
|
95
|
+
if (isPython) {
|
|
96
|
+
const cmd = linter?.includes('Ruff') ? 'ruff check .' : 'flake8 src/ --max-line-length=120 || true';
|
|
97
|
+
return ` - name: Lint\n run: |\n pip install ${linter?.includes('Ruff') ? 'ruff' : 'flake8'}\n ${cmd}`;
|
|
98
|
+
}
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────
|
|
103
|
+
// BLOC : Audit de dépendances
|
|
104
|
+
// ─────────────────────────────────────────────
|
|
105
|
+
function buildAuditBlock(isNode, isPython) {
|
|
106
|
+
if (isNode) {
|
|
107
|
+
return ` - name: Audit des dépendances
|
|
108
|
+
run: npm audit --audit-level=high`;
|
|
109
|
+
}
|
|
110
|
+
if (isPython) {
|
|
111
|
+
return ` - name: Audit des dépendances
|
|
112
|
+
run: |
|
|
113
|
+
pip install pip-audit
|
|
114
|
+
pip-audit`;
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─────────────────────────────────────────────
|
|
120
|
+
// BLOC : Tests + coverage
|
|
121
|
+
// ─────────────────────────────────────────────
|
|
122
|
+
function buildTestBlock(isNode, isPython, database) {
|
|
123
|
+
const dbEnv = database === 'PostgreSQL'
|
|
124
|
+
? `\n env:\n DATABASE_URL: postgresql://test:test@localhost:5432/test_db`
|
|
125
|
+
: database === 'MongoDB'
|
|
126
|
+
? `\n env:\n MONGODB_URI: mongodb://localhost:27017/test_db`
|
|
127
|
+
: '';
|
|
128
|
+
|
|
129
|
+
if (isNode) {
|
|
130
|
+
return ` - name: Tests (avec couverture)
|
|
131
|
+
run: npm test -- --coverage --if-present${dbEnv}
|
|
132
|
+
|
|
133
|
+
- name: Upload Coverage
|
|
134
|
+
uses: codecov/codecov-action@v4
|
|
135
|
+
with:
|
|
136
|
+
fail_ci_if_error: false`;
|
|
137
|
+
}
|
|
138
|
+
if (isPython) {
|
|
139
|
+
return ` - name: Tests (pytest + coverage)
|
|
140
|
+
run: |
|
|
141
|
+
pip install pytest pytest-cov
|
|
142
|
+
pytest tests/ -v --cov=src --cov-report=xml${dbEnv}
|
|
143
|
+
|
|
144
|
+
- name: Upload Coverage
|
|
145
|
+
uses: codecov/codecov-action@v4
|
|
146
|
+
with:
|
|
147
|
+
fail_ci_if_error: false`;
|
|
148
|
+
}
|
|
149
|
+
return ` - name: Tests\n run: echo "Ajoutez vos commandes de tests ici"`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────────────────────
|
|
153
|
+
// BLOC : Scan de sécurité (Gitleaks + Trivy FS)
|
|
154
|
+
// ─────────────────────────────────────────────
|
|
155
|
+
function buildSecurityScanBlock() {
|
|
156
|
+
return ` - name: Scan secrets (Gitleaks)
|
|
157
|
+
uses: gitleaks/gitleaks-action@v2
|
|
158
|
+
env:
|
|
159
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
160
|
+
|
|
161
|
+
- name: Scan filesystem (Trivy)
|
|
162
|
+
uses: aquasecurity/trivy-action@master
|
|
163
|
+
with:
|
|
164
|
+
scan-type: 'fs'
|
|
165
|
+
scan-ref: '.'
|
|
166
|
+
format: 'table'
|
|
167
|
+
exit-code: '1'
|
|
168
|
+
severity: 'CRITICAL,HIGH'`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────
|
|
172
|
+
// BLOC : Build & Push image Docker
|
|
173
|
+
// ─────────────────────────────────────────────
|
|
174
|
+
function buildDockerImageBlock(projectName, containerRegistry, cloud) {
|
|
175
|
+
if (!containerRegistry || containerRegistry.includes('Aucun')) return '';
|
|
176
|
+
|
|
177
|
+
let loginStep = '';
|
|
178
|
+
let imageTag = '';
|
|
179
|
+
|
|
180
|
+
if (containerRegistry.includes('Docker Hub')) {
|
|
181
|
+
loginStep = ` - name: Login Docker Hub
|
|
182
|
+
uses: docker/login-action@v3
|
|
183
|
+
with:
|
|
184
|
+
username: \${{ secrets.DOCKERHUB_USERNAME }}
|
|
185
|
+
password: \${{ secrets.DOCKERHUB_TOKEN }}`;
|
|
186
|
+
imageTag = `\${{ secrets.DOCKERHUB_USERNAME }}/${projectName}:\${{ github.sha }}`;
|
|
187
|
+
} else if (containerRegistry.includes('GHCR')) {
|
|
188
|
+
loginStep = ` - name: Login GitHub Container Registry
|
|
189
|
+
uses: docker/login-action@v3
|
|
190
|
+
with:
|
|
191
|
+
registry: ghcr.io
|
|
192
|
+
username: \${{ github.actor }}
|
|
193
|
+
password: \${{ secrets.GITHUB_TOKEN }}`;
|
|
194
|
+
imageTag = `ghcr.io/\${{ github.repository_owner }}/${projectName}:\${{ github.sha }}`;
|
|
195
|
+
} else if (containerRegistry.includes('ECR')) {
|
|
196
|
+
loginStep = ` - name: Login AWS ECR
|
|
197
|
+
uses: aws-actions/amazon-ecr-login@v2
|
|
198
|
+
with:
|
|
199
|
+
aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
200
|
+
aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
201
|
+
aws-region: eu-west-3`;
|
|
202
|
+
imageTag = `\${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-west-3.amazonaws.com/${projectName}:\${{ github.sha }}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return `
|
|
206
|
+
build-image:
|
|
207
|
+
name: Build & Push Image Docker
|
|
208
|
+
needs: quality-gate
|
|
209
|
+
runs-on: ubuntu-latest
|
|
210
|
+
outputs:
|
|
211
|
+
image: \${{ steps.meta.outputs.tags }}
|
|
212
|
+
steps:
|
|
213
|
+
- uses: actions/checkout@v4
|
|
214
|
+
|
|
215
|
+
${loginStep}
|
|
216
|
+
|
|
217
|
+
- name: Setup Docker Buildx
|
|
218
|
+
uses: docker/setup-buildx-action@v3
|
|
219
|
+
|
|
220
|
+
- name: Build et Push l'image
|
|
221
|
+
id: meta
|
|
222
|
+
uses: docker/build-push-action@v5
|
|
223
|
+
with:
|
|
224
|
+
context: ./docker
|
|
225
|
+
push: \${{ github.event_name != 'pull_request' }}
|
|
226
|
+
tags: ${imageTag}
|
|
227
|
+
cache-from: type=gha
|
|
228
|
+
cache-to: type=gha,mode=max
|
|
229
|
+
|
|
230
|
+
- name: Scan image Docker (Trivy)
|
|
231
|
+
uses: aquasecurity/trivy-action@master
|
|
232
|
+
with:
|
|
233
|
+
image-ref: ${imageTag}
|
|
234
|
+
format: 'table'
|
|
235
|
+
exit-code: '1'
|
|
236
|
+
severity: 'CRITICAL'`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─────────────────────────────────────────────
|
|
240
|
+
// BLOC : Deploy Staging + Health Check (Mission Critical)
|
|
241
|
+
// ─────────────────────────────────────────────
|
|
242
|
+
function buildStagingBlock(isMissionCritical, hasDockerImage) {
|
|
243
|
+
if (!isMissionCritical) return '';
|
|
244
|
+
const needs = hasDockerImage ? 'build-image' : 'quality-gate';
|
|
245
|
+
|
|
246
|
+
return `
|
|
247
|
+
deploy-staging:
|
|
248
|
+
name: Déploiement Staging
|
|
249
|
+
needs: ${needs}
|
|
250
|
+
if: github.ref == 'refs/heads/main'
|
|
251
|
+
runs-on: ubuntu-latest
|
|
252
|
+
environment: staging
|
|
253
|
+
steps:
|
|
254
|
+
- uses: actions/checkout@v4
|
|
255
|
+
|
|
256
|
+
- name: Deploy to Staging
|
|
257
|
+
run: |
|
|
258
|
+
echo "Ajoutez ici votre commande de déploiement vers l'environnement staging"
|
|
259
|
+
# Exemple : kubectl set image deployment/app app=IMAGE_TAG
|
|
260
|
+
|
|
261
|
+
- name: Health Check Staging
|
|
262
|
+
run: |
|
|
263
|
+
echo "Attente du démarrage de l'application..."
|
|
264
|
+
sleep 15
|
|
265
|
+
for i in {1..5}; do
|
|
266
|
+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.votre-domaine.com/health || true)
|
|
267
|
+
if [ "$STATUS" = "200" ]; then
|
|
268
|
+
echo "✅ Health check OK (tentative $i)"
|
|
269
|
+
exit 0
|
|
270
|
+
fi
|
|
271
|
+
echo "⏳ Tentative $i/5 échouée (status: $STATUS), nouvel essai dans 10s..."
|
|
272
|
+
sleep 10
|
|
273
|
+
done
|
|
274
|
+
echo "❌ Health check staging échoué après 5 tentatives"
|
|
275
|
+
exit 1
|
|
276
|
+
|
|
277
|
+
rollback-staging:
|
|
278
|
+
name: Rollback si Staging échoue
|
|
279
|
+
needs: deploy-staging
|
|
280
|
+
if: failure()
|
|
281
|
+
runs-on: ubuntu-latest
|
|
282
|
+
steps:
|
|
283
|
+
- name: Rollback
|
|
284
|
+
run: |
|
|
285
|
+
echo "⚠️ Déploiement staging en échec. Rollback vers la version précédente."
|
|
286
|
+
echo "Ajoutez ici votre commande de rollback (ex: kubectl rollout undo deployment/app)"`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─────────────────────────────────────────────
|
|
290
|
+
// BLOC : Deploy Production
|
|
291
|
+
// ─────────────────────────────────────────────
|
|
292
|
+
function buildDeployBlock(cloud, isMissionCritical, hasDockerImage) {
|
|
293
|
+
const needs = isMissionCritical ? 'deploy-staging' : (hasDockerImage ? 'build-image' : 'quality-gate');
|
|
294
|
+
const condition = isMissionCritical
|
|
295
|
+
? `github.ref == 'refs/heads/main' && needs.deploy-staging.result == 'success'`
|
|
296
|
+
: `github.ref == 'refs/heads/main'`;
|
|
297
|
+
|
|
298
|
+
if (cloud === 'Vercel') {
|
|
299
|
+
return `
|
|
300
|
+
deploy-production:
|
|
301
|
+
name: Déploiement Production (Vercel)
|
|
302
|
+
needs: ${needs}
|
|
303
|
+
if: ${condition}
|
|
304
|
+
runs-on: ubuntu-latest
|
|
305
|
+
environment: production
|
|
306
|
+
steps:
|
|
307
|
+
- uses: actions/checkout@v4
|
|
308
|
+
- name: Deploy to Vercel
|
|
309
|
+
uses: amondnet/vercel-action@v25
|
|
310
|
+
with:
|
|
311
|
+
vercel-token: \${{ secrets.VERCEL_TOKEN }}
|
|
312
|
+
vercel-org-id: \${{ secrets.VERCEL_ORG_ID }}
|
|
313
|
+
vercel-project-id: \${{ secrets.VERCEL_PROJECT_ID }}
|
|
314
|
+
vercel-args: '--prod'`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cloud === 'AWS') {
|
|
318
|
+
return `
|
|
319
|
+
deploy-production:
|
|
320
|
+
name: Déploiement Production (AWS)
|
|
321
|
+
needs: ${needs}
|
|
322
|
+
if: ${condition}
|
|
323
|
+
runs-on: ubuntu-latest
|
|
324
|
+
environment: production
|
|
325
|
+
steps:
|
|
326
|
+
- uses: actions/checkout@v4
|
|
327
|
+
- name: Configure AWS credentials
|
|
328
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
329
|
+
with:
|
|
330
|
+
aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
331
|
+
aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
332
|
+
aws-region: eu-west-3
|
|
333
|
+
- name: Deploy to AWS
|
|
334
|
+
run: echo "Ajoutez ici votre commande de déploiement AWS (ECS, EKS, Elastic Beanstalk...)"`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return `
|
|
338
|
+
release:
|
|
339
|
+
name: Release Automatique
|
|
340
|
+
needs: ${needs}
|
|
341
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
342
|
+
runs-on: ubuntu-latest
|
|
343
|
+
steps:
|
|
344
|
+
- uses: actions/checkout@v4
|
|
345
|
+
- name: Create GitHub Release
|
|
346
|
+
uses: softprops/action-gh-release@v1
|
|
347
|
+
with:
|
|
348
|
+
generate_release_notes: true`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─────────────────────────────────────────────
|
|
352
|
+
// EXPORT PRINCIPAL
|
|
353
|
+
// ─────────────────────────────────────────────
|
|
354
|
+
export function scaffoldCI(stack, cloud, projectName, options = {}) {
|
|
355
|
+
console.log(chalk.yellow('🤖 Génération du pipeline CI/CD complet...'));
|
|
356
|
+
fs.mkdirSync('.github/workflows', { recursive: true });
|
|
357
|
+
|
|
358
|
+
const { database = 'Aucune', strictness = 'Standard', linter = 'Aucun',
|
|
359
|
+
gitWorkflow = 'GitHub Flow', containerRegistry = 'Aucun', includeDocker = false } = options;
|
|
360
|
+
|
|
361
|
+
const isNode = stack.includes('Node') || stack.includes('React') || stack.includes('Next');
|
|
362
|
+
const isPython = stack.includes('Python');
|
|
363
|
+
const isMissionCritical = strictness.includes('Mission Critical');
|
|
364
|
+
const isStandard = strictness.includes('Standard') || isMissionCritical;
|
|
365
|
+
const hasDockerImage = includeDocker && !containerRegistry?.includes('Aucun') && cloud !== 'Vercel';
|
|
366
|
+
|
|
367
|
+
const serviceBlock = buildServiceBlock(database);
|
|
368
|
+
const serviceSection = serviceBlock ? `\n${serviceBlock}\n` : '';
|
|
369
|
+
|
|
370
|
+
const securityBlock = isStandard ? `\n${buildSecurityScanBlock()}` : '';
|
|
371
|
+
const auditBlock = isStandard ? `\n${buildAuditBlock(isNode, isPython)}` : '';
|
|
372
|
+
|
|
373
|
+
const ciContent = `name: GEF CI/CD — ${projectName}
|
|
374
|
+
|
|
375
|
+
${buildTriggers(gitWorkflow)}
|
|
376
|
+
|
|
377
|
+
jobs:
|
|
378
|
+
quality-gate:
|
|
379
|
+
name: Contrôle Qualité
|
|
380
|
+
runs-on: ubuntu-latest
|
|
381
|
+
${serviceSection}
|
|
382
|
+
steps:
|
|
383
|
+
- name: Checkout code
|
|
384
|
+
uses: actions/checkout@v4
|
|
385
|
+
with:
|
|
386
|
+
fetch-depth: 0
|
|
387
|
+
|
|
388
|
+
${buildRuntimeBlock(isNode, isPython)}
|
|
389
|
+
|
|
390
|
+
${buildLintBlock(isNode, isPython, linter)}
|
|
391
|
+
${auditBlock}
|
|
392
|
+
|
|
393
|
+
${buildTestBlock(isNode, isPython, database)}
|
|
394
|
+
${securityBlock}
|
|
395
|
+
${hasDockerImage ? buildDockerImageBlock(projectName, containerRegistry, cloud) : ''}
|
|
396
|
+
${isMissionCritical ? buildStagingBlock(true, hasDockerImage) : ''}
|
|
397
|
+
${buildDeployBlock(cloud, isMissionCritical, hasDockerImage)}
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
const RELEASE_PLEASE_YML = `on:
|
|
401
|
+
push:
|
|
402
|
+
branches:
|
|
403
|
+
- main
|
|
404
|
+
|
|
405
|
+
permissions:
|
|
406
|
+
contents: write
|
|
407
|
+
pull-requests: write
|
|
408
|
+
|
|
409
|
+
name: release-please
|
|
410
|
+
|
|
411
|
+
jobs:
|
|
412
|
+
release-please:
|
|
413
|
+
runs-on: ubuntu-latest
|
|
414
|
+
steps:
|
|
415
|
+
- uses: googleapis/release-please-action@v4
|
|
416
|
+
id: release
|
|
417
|
+
with:
|
|
418
|
+
release-type: node
|
|
419
|
+
|
|
420
|
+
- uses: actions/checkout@v4
|
|
421
|
+
if: \${{ steps.release.outputs.release_created }}
|
|
422
|
+
|
|
423
|
+
- uses: actions/setup-node@v4
|
|
424
|
+
with:
|
|
425
|
+
node-version: '20'
|
|
426
|
+
registry-url: 'https://registry.npmjs.org'
|
|
427
|
+
if: \${{ steps.release.outputs.release_created }}
|
|
428
|
+
|
|
429
|
+
- run: npm ci
|
|
430
|
+
if: \${{ steps.release.outputs.release_created }}
|
|
431
|
+
|
|
432
|
+
- run: npm publish
|
|
433
|
+
env:
|
|
434
|
+
NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
435
|
+
if: \${{ steps.release.outputs.release_created }}
|
|
436
|
+
`;
|
|
437
|
+
|
|
438
|
+
fs.writeFileSync('.github/workflows/main.yml', ciContent);
|
|
439
|
+
fs.writeFileSync('.github/workflows/release-please.yml', RELEASE_PLEASE_YML);
|
|
440
|
+
console.log(chalk.green('✅ Pipeline CI/CD complet généré.'));
|
|
441
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// features/scaffold-docker.js — Génération des fichiers Docker
|
|
2
|
+
// Réf. Playbook §1 : SRP — une fonction par stack Docker, nesting <= 3
|
|
3
|
+
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
const DOCKERIGNORE = [
|
|
8
|
+
'node_modules', 'npm-debug.log', '.git', '.gitignore',
|
|
9
|
+
'.env', '*.log', 'dist', 'build', '__pycache__', 'venv', '.venv',
|
|
10
|
+
].join('\n') + '\n';
|
|
11
|
+
|
|
12
|
+
const DOCKERFILES = {
|
|
13
|
+
'Next.js (React)': `# Étape 1 : Build
|
|
14
|
+
FROM node:20-alpine AS builder
|
|
15
|
+
WORKDIR /app
|
|
16
|
+
COPY package*.json ./
|
|
17
|
+
RUN npm ci
|
|
18
|
+
COPY . .
|
|
19
|
+
RUN npm run build
|
|
20
|
+
|
|
21
|
+
# Étape 2 : Production
|
|
22
|
+
FROM node:20-alpine AS runner
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
ENV NODE_ENV=production
|
|
25
|
+
COPY --from=builder /app/.next/standalone ./
|
|
26
|
+
COPY --from=builder /app/.next/static ./.next/static
|
|
27
|
+
COPY --from=builder /app/public ./public
|
|
28
|
+
EXPOSE 3000
|
|
29
|
+
CMD ["node", "server.js"]
|
|
30
|
+
`,
|
|
31
|
+
'React (Vite)': `# Étape 1 : Build
|
|
32
|
+
FROM node:20-alpine AS builder
|
|
33
|
+
WORKDIR /app
|
|
34
|
+
COPY package*.json ./
|
|
35
|
+
RUN npm ci
|
|
36
|
+
COPY . .
|
|
37
|
+
RUN npm run build
|
|
38
|
+
|
|
39
|
+
# Étape 2 : Serveur statique Nginx
|
|
40
|
+
FROM nginx:alpine AS runner
|
|
41
|
+
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
42
|
+
EXPOSE 80
|
|
43
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
44
|
+
`,
|
|
45
|
+
'API Node.js (Express)': `FROM node:20-alpine
|
|
46
|
+
WORKDIR /app
|
|
47
|
+
COPY package*.json ./
|
|
48
|
+
RUN npm ci --only=production
|
|
49
|
+
COPY . .
|
|
50
|
+
EXPOSE 3000
|
|
51
|
+
CMD ["node", "src/index.js"]
|
|
52
|
+
`,
|
|
53
|
+
'API Python (FastAPI)': `FROM python:3.12-slim
|
|
54
|
+
WORKDIR /app
|
|
55
|
+
COPY requirements.txt ./
|
|
56
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
57
|
+
COPY . .
|
|
58
|
+
EXPOSE 8000
|
|
59
|
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
60
|
+
`,
|
|
61
|
+
default: `FROM alpine:latest
|
|
62
|
+
WORKDIR /app
|
|
63
|
+
COPY . .
|
|
64
|
+
CMD ["sh"]
|
|
65
|
+
`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Génère un snippet docker-compose pour un service de base de données.
|
|
70
|
+
* @param {string} database
|
|
71
|
+
* @param {string} projectName
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function buildDbService(database, projectName) {
|
|
75
|
+
const dbName = projectName.toLowerCase().replace(/\s+/g, '_');
|
|
76
|
+
if (database === 'PostgreSQL') {
|
|
77
|
+
return `
|
|
78
|
+
db:
|
|
79
|
+
image: postgres:16-alpine
|
|
80
|
+
environment:
|
|
81
|
+
POSTGRES_USER: user
|
|
82
|
+
POSTGRES_PASSWORD: password
|
|
83
|
+
POSTGRES_DB: ${dbName}
|
|
84
|
+
ports:
|
|
85
|
+
- "5432:5432"
|
|
86
|
+
volumes:
|
|
87
|
+
- db_data:/var/lib/postgresql/data
|
|
88
|
+
- ../database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
89
|
+
|
|
90
|
+
volumes:
|
|
91
|
+
db_data:`;
|
|
92
|
+
}
|
|
93
|
+
if (database === 'MongoDB') {
|
|
94
|
+
return `
|
|
95
|
+
db:
|
|
96
|
+
image: mongo:7
|
|
97
|
+
ports:
|
|
98
|
+
- "27017:27017"
|
|
99
|
+
volumes:
|
|
100
|
+
- db_data:/data/db
|
|
101
|
+
|
|
102
|
+
volumes:
|
|
103
|
+
db_data:`;
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Génère le contenu docker-compose selon la stack.
|
|
110
|
+
* @param {string} stack
|
|
111
|
+
* @param {string} database
|
|
112
|
+
* @param {string} projectName
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function buildComposeContent(stack, database, projectName) {
|
|
116
|
+
const port = stack.includes('Python') ? '8000' : stack.includes('Vite') ? '80' : '3000';
|
|
117
|
+
const dbService = buildDbService(database, projectName);
|
|
118
|
+
const needsDb = stack.includes('Express') || stack.includes('Python');
|
|
119
|
+
|
|
120
|
+
return `version: '3.8'
|
|
121
|
+
services:
|
|
122
|
+
app:
|
|
123
|
+
build:
|
|
124
|
+
context: ..
|
|
125
|
+
dockerfile: docker/Dockerfile
|
|
126
|
+
ports:
|
|
127
|
+
- "${port}:${port}"
|
|
128
|
+
${needsDb ? 'env_file:\n - ../.env\n depends_on:\n - db' : ''}${dbService}
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Génère tous les fichiers Docker pour le projet.
|
|
134
|
+
* @param {string} stack
|
|
135
|
+
* @param {string} database
|
|
136
|
+
* @param {string} projectName
|
|
137
|
+
*/
|
|
138
|
+
export function scaffoldDocker(stack, database, projectName) {
|
|
139
|
+
console.log(chalk.yellow('🐳 Génération des fichiers Docker...'));
|
|
140
|
+
fs.mkdirSync('docker', { recursive: true });
|
|
141
|
+
fs.writeFileSync('.dockerignore', DOCKERIGNORE);
|
|
142
|
+
|
|
143
|
+
const dockerfile = DOCKERFILES[stack] ?? DOCKERFILES.default;
|
|
144
|
+
fs.writeFileSync('docker/Dockerfile', dockerfile);
|
|
145
|
+
fs.writeFileSync('docker/docker-compose.yml', buildComposeContent(stack, database, projectName));
|
|
146
|
+
|
|
147
|
+
if (database === 'PostgreSQL') {
|
|
148
|
+
fs.mkdirSync('database', { recursive: true });
|
|
149
|
+
fs.writeFileSync('database/init.sql', `-- Initialisation de la base de données ${projectName}
|
|
150
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
151
|
+
id SERIAL PRIMARY KEY,
|
|
152
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
153
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
154
|
+
);
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(chalk.green('✅ Fichiers Docker générés.'));
|
|
159
|
+
}
|