From 7e3d2ae29f2af3d869a6d202ba53a5d8ddb52f7f Mon Sep 17 00:00:00 2001 From: saqib mir Date: Thu, 3 Sep 2026 14:19:21 +0530 Subject: [PATCH] add the new feaures --- Jenkinsfile | 226 --------------------------------- docker-compose.development.yml | 31 +---- docker-compose.test.yml | 25 +--- 3 files changed, 2 insertions(+), 280 deletions(-) delete mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index e555db9..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,226 +0,0 @@ -// CI/CD pipeline for supporthub-api. -// -// Stage order and guarantees are defined by: -// - .specify/memory/constitution.md -> "Testing, Observability & CI/CD Gates" -// - specs/001-ci-pipeline/contracts/pipeline-stage-contract.md -// -// Prerequisites (configured on the Jenkins side, never in this repo): -// - Agents with Docker and Node.js 20+ available (label: 'docker && node20') -// - Credentials (Secret text unless noted) per target environment in [test, prod]: -// -postgres-password, -redis-password, -jwt-secret -// -aws-access-key-id, -aws-secret-access-key -// -integration-credential-encryption-key (64 hex chars / 32 bytes — see -// specs/002-saas-integration/research.md "Credential storage") -// Plus one Username/Password credential: docker-registry-credentials -// - A DOCKER_REGISTRY value (e.g. via a "CI_DOCKER_REGISTRY" global Jenkins env var, -// or override the default below) pointing at the org's actual image registry. -// -// This file intentionally contains no secret values, only credential IDs — see -// specs/001-ci-pipeline/research.md "Environment/secrets handling in the pipeline". - -pipeline { - agent { label 'docker && node20' } - - options { - timestamps() - buildDiscarder(logRotator(numToKeepStr: '30')) - // Each build gets its own workspace by default; Compose project names below are - // additionally scoped by BUILD_NUMBER so concurrent runs never share containers, - // networks, or volumes (FR-009 / SC-005). - ansiColor('xterm') - } - - environment { - DOCKER_REGISTRY = "${env.CI_DOCKER_REGISTRY ?: 'registry.example.com/supporthub'}" - IMAGE_NAME = 'supporthub-api' - COMPOSE_PROJECT = "supporthub-ci-${env.BUILD_NUMBER}" - } - - stages { - - stage('Checkout') { - steps { - checkout scm - } - } - - stage('Install') { - steps { - sh 'npm ci' - } - } - - stage('Environment validation') { - steps { - script { - resolveDeployTarget() - writeTargetEnvFile(env.TARGET_ENV == 'none' ? 'test' : env.TARGET_ENV) - } - // Reuses the existing Zod schema in src/config/env.ts as-is: it throws a - // specific, descriptive error on safeParse failure, so this fails fast with - // no new validation logic (FR-002). - sh "npx tsx --env-file=.env.${env.TARGET_ENV == 'none' ? 'test' : env.TARGET_ENV} -e \"import('./src/config/env.ts').then(() => console.log('Environment OK'))\"" - } - } - - stage('Generate Prisma client') { - steps { - sh 'npm run prisma:generate' - } - } - - stage('Typecheck') { - steps { - sh 'npm run typecheck' - } - } - - stage('Lint') { - steps { - sh 'npm run lint' - // Enforces Constitution Principle III (module boundaries) server-side — - // mirrors .husky/pre-commit so a bypassed/missing local hook can't merge - // a boundary violation. - sh 'npx tsx scripts/check-architecture.ts' - } - } - - stage('Format check') { - steps { - sh 'npm run format:check' - } - } - - stage('Unit test') { - steps { - sh 'npm run test:unit' - } - } - - stage('Integration test') { - steps { - sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env.test -f docker-compose.test.yml up -d --wait postgres redis minio" - sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env.test -f docker-compose.test.yml run --rm --build app npm run test:integration" - } - } - - stage('E2E test') { - steps { - sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env.test -f docker-compose.test.yml run --rm app npm run test:e2e" - } - } - - stage('Build') { - steps { - script { - def buildTarget = env.TARGET_ENV == 'none' ? 'test' : env.TARGET_ENV - sh "npm run build:${buildTarget}" - } - } - } - - stage('Docker build') { - steps { - script { - def buildTarget = env.TARGET_ENV == 'none' ? 'test' : env.TARGET_ENV - env.IMAGE_TAG = "${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${buildTarget}-${env.BUILD_NUMBER}" - } - sh "docker build --build-arg BUILD_COMMAND=\"npm run build:${env.TARGET_ENV == 'none' ? 'test' : env.TARGET_ENV}\" -t ${env.IMAGE_TAG} ." - } - } - - stage('Publish') { - when { expression { env.TARGET_ENV != 'none' } } - steps { - withCredentials([usernamePassword(credentialsId: 'docker-registry-credentials', - usernameVariable: 'REGISTRY_USER', - passwordVariable: 'REGISTRY_PASSWORD')]) { - sh ''' - echo "$REGISTRY_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$REGISTRY_USER" --password-stdin - docker push "$IMAGE_TAG" - docker logout "$DOCKER_REGISTRY" - ''' - } - } - } - - stage('Deploy') { - when { expression { env.TARGET_ENV != 'none' } } - steps { - script { - writeTargetEnvFile(env.TARGET_ENV) - } - sh "docker compose --env-file .env.${env.TARGET_ENV} -f docker-compose.${env.TARGET_ENV == 'prod' ? 'prod' : 'test'}.yml up -d" - } - } - } - - post { - always { - // Tear down the ephemeral integration/E2E stack regardless of outcome, and never - // leave a generated .env.* file behind in the workspace. --env-file must be passed - // to `down` too, or Compose can fail to resolve service config and leave containers - // running (confirmed while validating this pipeline locally). - sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env.test -f docker-compose.test.yml down -v --remove-orphans || true" - sh 'rm -f .env.test .env.prod .env.development' - cleanWs() - } - failure { - echo "Pipeline failed at stage: ${currentBuild.result}. See the failing stage's log above for the exact command and output — no local reproduction should be necessary (FR-004)." - } - } -} - -// Resolves env.TARGET_ENV from the branch being built: -// main -> prod -// develop/test -> test -// anything else -> none (validate-only; Publish/Deploy stages are skipped, not failed) -void resolveDeployTarget() { - def branch = env.BRANCH_NAME ?: '' - if (branch == 'main') { - env.TARGET_ENV = 'prod' - } else if (branch == 'develop' || branch == 'test') { - env.TARGET_ENV = 'test' - } else { - env.TARGET_ENV = 'none' - } - echo "Resolved deploy target for branch '${branch}': ${env.TARGET_ENV}" -} - -// Writes .env. into the workspace from Jenkins credentials — never read from a -// file committed to the repository (constitution governance: "secrets never committed"). -void writeTargetEnvFile(String target) { - withCredentials([ - string(credentialsId: "${target}-postgres-password", variable: 'POSTGRES_PASSWORD'), - string(credentialsId: "${target}-redis-password", variable: 'REDIS_PASSWORD'), - string(credentialsId: "${target}-jwt-secret", variable: 'JWT_SECRET'), - string(credentialsId: "${target}-aws-access-key-id", variable: 'AWS_ACCESS_KEY_ID'), - string(credentialsId: "${target}-aws-secret-access-key", variable: 'AWS_SECRET_ACCESS_KEY'), - string(credentialsId: "${target}-integration-credential-encryption-key", variable: 'INTEGRATION_CREDENTIAL_ENCRYPTION_KEY'), - ]) { - def port = target == 'prod' ? '4503' : (target == 'test' ? '4502' : '4501') - def dbName = target == 'prod' ? 'myapp_prod' : (target == 'test' ? 'myapp_test' : 'support_dev') - def dbUser = target == 'prod' ? 'myapp_prod' : (target == 'test' ? 'myapp_test' : 'support_user') - writeFile file: ".env.${target}", text: """ -NODE_ENV=${target == 'prod' ? 'production' : target} -PORT=${port} -BUILD_COMMAND=npm run build:${target} -POSTGRES_HOST=postgres -POSTGRES_DB=${dbName} -POSTGRES_USER=${dbUser} -POSTGRES_PASSWORD=${POSTGRES_PASSWORD} -DATABASE_URL=postgresql://${dbUser}:${POSTGRES_PASSWORD}@postgres:5432/${dbName} -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_PASSWORD=${REDIS_PASSWORD} -JWT_SECRET=${JWT_SECRET} -AWS_REGION=us-east-1 -AWS_S3_BUCKET=supporthub-attachments-${target} -AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -${target == 'prod' ? '' : 'AWS_S3_ENDPOINT=http://minio:9000'} -INTEGRATION_CREDENTIAL_ENCRYPTION_KEY=${INTEGRATION_CREDENTIAL_ENCRYPTION_KEY} -CORS_ORIGINS=${target == 'prod' ? 'https://app.supporthub.com,https://admin.supporthub.com' : 'http://localhost:3000'} -""".stripIndent().trim() - } -} diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 157a40d..4fbdbed 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -27,8 +27,6 @@ services: condition: service_healthy redis: condition: service_healthy - minio: - condition: service_healthy restart: unless-stopped @@ -86,33 +84,6 @@ services: restart: unless-stopped - minio: - image: minio/minio:latest - - container_name: minio-development - - command: server /data --console-address ":9001" - - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - - ports: - - "9000:9000" - - "9001:9001" - - volumes: - - minio_development_data:/data - - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 10 - - restart: unless-stopped - volumes: postgres_development_data: - redis_development_data: - minio_development_data: \ No newline at end of file + redis_development_data: \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 8420131..aeae661 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -27,8 +27,6 @@ services: condition: service_healthy redis: condition: service_healthy - minio: - condition: service_healthy restart: unless-stopped @@ -82,27 +80,6 @@ services: restart: unless-stopped - minio: - image: minio/minio:latest - - command: server /data --console-address ":9001" - - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - - volumes: - - minio_test_data:/data - - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 10 - - restart: unless-stopped - volumes: postgres_test_data: - redis_test_data: - minio_test_data: \ No newline at end of file + redis_test_data: \ No newline at end of file