Skip to content

Commit dbbd1ff

Browse files
Mbd06bclaude
andcommitted
build: add Jenkinsfile and SonarQube config for CI pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0d742e0 commit dbbd1ff

2 files changed

Lines changed: 382 additions & 0 deletions

File tree

Jenkinsfile

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
/**
2+
* Sophia Pipeline (elohim-sophia)
3+
*
4+
* Builds and publishes the Sophia rendering library (React/TypeScript monorepo).
5+
* Triggered by orchestrator when sophia/ files change.
6+
*
7+
* What this pipeline builds:
8+
* - Sophia monorepo packages (lint, typecheck, test, build)
9+
* - sophia-element UMD bundle for downstream consumption
10+
*
11+
* Environment Architecture:
12+
* - main -> sophia SonarQube project (blocking gate)
13+
* - staging* -> sophia-staging SonarQube project
14+
* - dev/* -> sophia-alpha SonarQube project
15+
*
16+
* Trigger behavior:
17+
* - Only runs when triggered by orchestrator or manual
18+
* - Shows NOT_BUILT when triggered directly by webhook
19+
*
20+
* Artifact dependency:
21+
* - Downstream: elohim-app consumes sophia-element UMD bundle + CSS
22+
*
23+
* @see orchestrator/Jenkinsfile for central trigger logic
24+
*/
25+
26+
// ============================================================================
27+
// HELPER FUNCTIONS
28+
// ============================================================================
29+
30+
/**
31+
* Determine SonarQube project config based on branch.
32+
* Returns: [projectKey: String, shouldEnforce: Boolean, env: String]
33+
*/
34+
@NonCPS
35+
def getSonarProjectConfig() {
36+
def targetBranch = env.CHANGE_TARGET ?: env.BRANCH_NAME
37+
38+
if (targetBranch == 'main') {
39+
return [projectKey: 'sophia', shouldEnforce: true, env: 'prod']
40+
} else if (targetBranch == 'staging' || targetBranch ==~ /staging-.+/) {
41+
return [projectKey: 'sophia-staging', shouldEnforce: false, env: 'staging']
42+
} else {
43+
return [projectKey: 'sophia-alpha', shouldEnforce: false, env: 'alpha']
44+
}
45+
}
46+
47+
pipeline {
48+
agent {
49+
kubernetes {
50+
cloud 'kubernetes'
51+
yaml '''
52+
apiVersion: v1
53+
kind: Pod
54+
spec:
55+
nodeSelector:
56+
node-type: operations
57+
tolerations:
58+
- key: "workload-type"
59+
operator: "Equal"
60+
value: "build"
61+
effect: "NoSchedule"
62+
containers:
63+
- name: node
64+
image: node:20
65+
command: [cat]
66+
tty: true
67+
resources:
68+
requests:
69+
memory: "4Gi"
70+
cpu: "2"
71+
ephemeral-storage: "5Gi"
72+
limits:
73+
memory: "8Gi"
74+
cpu: "4"
75+
ephemeral-storage: "10Gi"
76+
'''
77+
}
78+
}
79+
80+
parameters {
81+
booleanParam(
82+
name: 'FORCE_BUILD',
83+
defaultValue: false,
84+
description: 'Force full rebuild even without code changes'
85+
)
86+
}
87+
88+
options {
89+
timeout(time: 30, unit: 'MINUTES')
90+
disableConcurrentBuilds(abortPrevious: true)
91+
buildDiscarder(logRotator(numToKeepStr: '50'))
92+
overrideIndexTriggers(false) // Only orchestrator or manual triggers - no webhook/branch indexing
93+
}
94+
95+
// No triggers - orchestrator handles all webhook events
96+
97+
stages {
98+
stage('Check Trigger') {
99+
steps {
100+
script {
101+
def validTrigger = currentBuild.getBuildCauses().any { cause ->
102+
cause._class.contains('UserIdCause') ||
103+
cause._class.contains('UpstreamCause')
104+
}
105+
if (!validTrigger) {
106+
echo "Pipeline skipped - use orchestrator"
107+
currentBuild.result = 'NOT_BUILT'
108+
currentBuild.displayName = "#${env.BUILD_NUMBER} SKIPPED"
109+
env.PIPELINE_SKIPPED = 'true'
110+
} else {
111+
echo "Valid trigger: ${currentBuild.getBuildCauses()*.shortDescription.join(', ')}"
112+
}
113+
}
114+
}
115+
}
116+
117+
stage('Checkout') {
118+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
119+
steps {
120+
container('node') {
121+
checkout scm
122+
echo "Building Sophia for branch: ${env.BRANCH_NAME}"
123+
}
124+
}
125+
}
126+
127+
stage('Install') {
128+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
129+
steps {
130+
container('node') {
131+
dir('sophia') {
132+
sh '''#!/bin/bash
133+
set -euo pipefail
134+
135+
# Install pnpm globally
136+
corepack enable
137+
corepack prepare pnpm@latest --activate
138+
139+
pnpm install --frozen-lockfile
140+
'''
141+
}
142+
}
143+
}
144+
}
145+
146+
stage('Lint') {
147+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
148+
steps {
149+
container('node') {
150+
dir('sophia') {
151+
sh '''#!/bin/bash
152+
set -euo pipefail
153+
pnpm lint
154+
'''
155+
}
156+
}
157+
}
158+
}
159+
160+
stage('Type Check') {
161+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
162+
steps {
163+
container('node') {
164+
dir('sophia') {
165+
sh '''#!/bin/bash
166+
set -euo pipefail
167+
pnpm typecheck
168+
'''
169+
}
170+
}
171+
}
172+
}
173+
174+
stage('Unit Tests') {
175+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
176+
steps {
177+
container('node') {
178+
dir('sophia') {
179+
sh '''#!/bin/bash
180+
set -euo pipefail
181+
pnpm test -- --ci --coverage
182+
'''
183+
}
184+
}
185+
}
186+
}
187+
188+
stage('Build') {
189+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
190+
steps {
191+
container('node') {
192+
dir('sophia') {
193+
sh '''#!/bin/bash
194+
set -euo pipefail
195+
pnpm build
196+
pnpm build:types
197+
'''
198+
}
199+
}
200+
}
201+
}
202+
203+
stage('Build UMD') {
204+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
205+
steps {
206+
container('node') {
207+
dir('sophia') {
208+
sh '''#!/bin/bash
209+
set -euo pipefail
210+
211+
pnpm --filter @ethosengine/sophia-element build:umd
212+
213+
# Verify UMD bundle was produced
214+
UMD_PATH="packages/sophia-element/dist/sophia-element.umd.js"
215+
if [ ! -f "$UMD_PATH" ]; then
216+
echo "ERROR: UMD bundle not found at $UMD_PATH"
217+
exit 1
218+
fi
219+
220+
UMD_SIZE=$(stat -c%s "$UMD_PATH" 2>/dev/null || stat -f%z "$UMD_PATH")
221+
echo "UMD bundle: $UMD_PATH ($UMD_SIZE bytes)"
222+
'''
223+
}
224+
}
225+
}
226+
}
227+
228+
stage('SonarQube Analysis') {
229+
when {
230+
allOf {
231+
expression { env.PIPELINE_SKIPPED != 'true' }
232+
anyOf {
233+
branch 'main'
234+
branch 'staging'
235+
branch 'dev'
236+
expression { env.BRANCH_NAME ==~ /staging-.+/ }
237+
expression { env.BRANCH_NAME ==~ /feat-.+/ }
238+
expression { env.BRANCH_NAME ==~ /claude\/.+/ }
239+
changeRequest target: 'main'
240+
changeRequest target: 'staging'
241+
changeRequest target: 'dev'
242+
}
243+
}
244+
}
245+
steps {
246+
container('node') {
247+
dir('sophia') {
248+
script {
249+
def sonarConfig = getSonarProjectConfig()
250+
echo "SonarQube Analysis: project=${sonarConfig.projectKey}, env=${sonarConfig.env}, enforce=${sonarConfig.shouldEnforce}"
251+
252+
withSonarQubeEnv('ee-sonarqube') {
253+
sh """
254+
sonar-scanner \
255+
-Dsonar.projectKey=${sonarConfig.projectKey} \
256+
-Dsonar.sources=packages \
257+
-Dsonar.tests=packages \
258+
-Dsonar.test.inclusions=**/*.test.ts,**/*.test.tsx \
259+
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \
260+
-Dsonar.coverage.exclusions=**/*.test.ts,**/*.test.tsx,**/*.stories.tsx,**/dist/**,**/__docs__/**,**/node_modules/** \
261+
-Dsonar.qualitygate.wait=false
262+
"""
263+
}
264+
265+
echo "Waiting for SonarQube quality gate..."
266+
try {
267+
timeout(time: 10, unit: 'MINUTES') {
268+
def qg = waitForQualityGate abortPipeline: false
269+
if (qg.status != 'OK') {
270+
if (sonarConfig.shouldEnforce) {
271+
// Production: Block on quality gate failure
272+
error "SonarQube Quality Gate FAILED: ${qg.status}\nReview issues at: ${env.SONAR_HOST_URL}/dashboard?id=${sonarConfig.projectKey}"
273+
} else {
274+
// Alpha/Staging: Log warning but don't block
275+
echo "SonarQube Quality Gate status: ${qg.status}"
276+
echo "Review issues at: ${env.SONAR_HOST_URL}/dashboard?id=${sonarConfig.projectKey}"
277+
currentBuild.result = 'UNSTABLE'
278+
}
279+
} else {
280+
echo "SonarQube quality gate passed (${sonarConfig.env})"
281+
}
282+
}
283+
} catch (Exception e) {
284+
echo "SonarQube quality gate check failed: ${e.message}"
285+
echo "This may be due to webhook configuration issues."
286+
echo "Review results at: ${env.SONAR_HOST_URL}/dashboard?id=${sonarConfig.projectKey}"
287+
if (sonarConfig.shouldEnforce) {
288+
currentBuild.result = 'UNSTABLE'
289+
echo "Marking build UNSTABLE - production quality gate could not be verified"
290+
} else {
291+
echo "Continuing pipeline..."
292+
}
293+
}
294+
}
295+
}
296+
}
297+
}
298+
}
299+
300+
stage('Publish') {
301+
when {
302+
allOf {
303+
expression { env.PIPELINE_SKIPPED != 'true' }
304+
anyOf {
305+
branch 'main'
306+
branch 'staging'
307+
}
308+
}
309+
}
310+
steps {
311+
container('node') {
312+
dir('sophia') {
313+
script {
314+
withCredentials([string(credentialsId: 'npm-publish-token', variable: 'NPM_TOKEN')]) {
315+
sh '''#!/bin/bash
316+
set -euo pipefail
317+
318+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
319+
pnpm --filter @ethosengine/sophia-element publish --no-git-checks
320+
'''
321+
}
322+
echo "Published @ethosengine/sophia-element to npm registry"
323+
}
324+
}
325+
}
326+
}
327+
}
328+
329+
stage('Archive Artifacts') {
330+
when { expression { env.PIPELINE_SKIPPED != 'true' } }
331+
steps {
332+
container('node') {
333+
dir('sophia') {
334+
script {
335+
// Stash UMD bundle + CSS for downstream pipelines (e.g. elohim-app)
336+
stash(
337+
name: 'sophia-umd',
338+
includes: 'packages/sophia-element/dist/**'
339+
)
340+
archiveArtifacts(
341+
artifacts: 'packages/sophia-element/dist/sophia-element.umd.js,packages/sophia-element/dist/**/*.css',
342+
allowEmptyArchive: false
343+
)
344+
echo "Archived sophia-element UMD bundle and CSS for downstream consumption"
345+
}
346+
}
347+
}
348+
}
349+
}
350+
}
351+
352+
post {
353+
always {
354+
script {
355+
if (env.PIPELINE_SKIPPED != 'true') {
356+
def sonarConfig = getSonarProjectConfig()
357+
currentBuild.description = [
358+
"branch:${env.BRANCH_NAME}",
359+
"sonar:${sonarConfig.projectKey}"
360+
].join(' | ')
361+
}
362+
}
363+
}
364+
success {
365+
echo "Sophia pipeline completed successfully"
366+
}
367+
failure {
368+
echo "Sophia pipeline failed"
369+
echo "Check the logs above for details. Common issues:"
370+
echo " - pnpm install failures: Check lockfile consistency"
371+
echo " - Lint/typecheck errors: Fix source code issues"
372+
echo " - UMD build missing: Ensure sophia-element build:umd script is defined"
373+
}
374+
}
375+
}

sonar-project.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
sonar.projectKey=sophia-alpha
2+
sonar.sources=packages
3+
sonar.tests=packages
4+
sonar.test.inclusions=**/*.test.ts,**/*.test.tsx
5+
sonar.javascript.lcov.reportPaths=coverage/lcov.info
6+
sonar.eslint.reportPaths=eslint-report.json
7+
sonar.host.url=https://sonarqube.ethosengine.com

0 commit comments

Comments
 (0)