Skip to content

Commit f0ce7f3

Browse files
committed
feat(ci/cd): enhance CI/CD testing and deployment pipelines
1 parent c0fd89f commit f0ce7f3

File tree

15 files changed

+2231
-1332
lines changed

15 files changed

+2231
-1332
lines changed

.github/workflows/ci.yml

Lines changed: 81 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,132 @@
11
name: CI/CD Pipeline for Budget Management API
2+
23
on:
34
push:
4-
branches: [master]
5+
branches:
6+
- master
57
pull_request:
6-
branches: [master]
8+
branches:
9+
- master
710

811
jobs:
912
formatting:
10-
name: "🔧 Formatting & Lint"
13+
name: 🔧 Install, Lint & Format
1114
runs-on: ubuntu-latest
1215
steps:
1316
- name: Checkout code
1417
uses: actions/checkout@v3
15-
continue-on-error: true
1618

17-
- name: Setup Node.js
19+
- name: Use Node.js 18
1820
uses: actions/setup-node@v3
1921
with:
2022
node-version: 18
21-
continue-on-error: true
2223

2324
- name: Install dependencies
2425
run: npm ci
25-
continue-on-error: true
2626

27-
- name: Prettier format
27+
- name: Run Prettier
2828
run: npm run format
29-
continue-on-error: true
30-
31-
- name: ESLint lint
32-
run: npm run lint
33-
continue-on-error: true
3429

35-
backend:
36-
name: "✅ Backend Build & Test"
30+
test:
31+
name: ✅ Run Tests
3732
runs-on: ubuntu-latest
38-
needs: [formatting]
39-
if: ${{ always() }}
33+
needs: formatting
4034
steps:
4135
- name: Checkout code
4236
uses: actions/checkout@v3
43-
continue-on-error: true
4437

45-
- name: Setup Node.js
38+
- name: Use Node.js 18
4639
uses: actions/setup-node@v3
4740
with:
4841
node-version: 18
49-
continue-on-error: true
50-
51-
- name: Install backend deps
52-
run: npm --workspace backend ci
53-
continue-on-error: true
54-
55-
- name: Backend format
56-
run: npm --workspace backend run format
57-
continue-on-error: true
58-
59-
- name: Backend lint
60-
run: npm --workspace backend run lint
61-
continue-on-error: true
6242

63-
- name: Build backend
64-
run: npm --workspace backend run build
65-
continue-on-error: true
43+
- name: Install dependencies
44+
run: npm ci
6645

67-
- name: Backend tests
68-
run: npm --workspace backend run test
69-
continue-on-error: true
46+
- name: Run Jest tests
47+
run: npm test
7048

71-
frontend:
72-
name: "🌐 Frontend Build & Test"
49+
coverage:
50+
name: 📊 Generate Coverage
7351
runs-on: ubuntu-latest
74-
needs: [backend]
75-
if: ${{ always() }}
52+
needs: test
7653
steps:
7754
- name: Checkout code
7855
uses: actions/checkout@v3
79-
continue-on-error: true
8056

81-
- name: Setup Node.js
57+
- name: Use Node.js 18
8258
uses: actions/setup-node@v3
8359
with:
8460
node-version: 18
85-
continue-on-error: true
8661

87-
- name: Install frontend deps
88-
run: npm --workspace frontend ci
89-
continue-on-error: true
62+
- name: Install dependencies
63+
run: npm ci
64+
65+
- name: Run coverage
66+
run: npm run coverage
9067

91-
- name: Frontend format
92-
run: npm --workspace frontend run format
93-
continue-on-error: true
68+
- name: Archive coverage report
69+
if: always()
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: coverage-report
73+
path: coverage
9474

95-
- name: Frontend lint
96-
run: npm --workspace frontend run lint
97-
continue-on-error: true
75+
docker:
76+
name: 🐳 Build & Push Docker Images
77+
runs-on: ubuntu-latest
78+
needs:
79+
- coverage
80+
permissions:
81+
contents: read
82+
packages: write
83+
steps:
84+
- name: Checkout code
85+
uses: actions/checkout@v3
9886

99-
- name: Build frontend
100-
run: npm --workspace frontend run build
101-
continue-on-error: true
87+
- name: Log in to GitHub Container Registry
88+
uses: docker/login-action@v2
89+
with:
90+
registry: ghcr.io
91+
username: ${{ github.actor }}
92+
password: ${{ secrets.GITHUB_TOKEN }}
10293

103-
- name: Frontend tests
104-
run: npm --workspace frontend run test:unit
105-
continue-on-error: true
94+
- name: Build & push backend image
95+
uses: docker/build-push-action@v3
96+
with:
97+
context: .
98+
file: ./Dockerfile
99+
push: true
100+
tags: |
101+
ghcr.io/${{ github.repository_owner }}/budget-management-backend:${{ github.sha }}
102+
ghcr.io/${{ github.repository_owner }}/budget-management-backend:latest
103+
104+
- name: Build & push frontend image
105+
uses: docker/build-push-action@v3
106+
with:
107+
context: ./frontend
108+
file: ./frontend/Dockerfile
109+
push: true
110+
tags: |
111+
ghcr.io/${{ github.repository_owner }}/budget-management-frontend:${{ github.sha }}
112+
ghcr.io/${{ github.repository_owner }}/budget-management-frontend:latest
113+
114+
deploy:
115+
name: 🚀 Deploying
116+
runs-on: ubuntu-latest
117+
needs: docker
118+
steps:
119+
- name: Simulate deployment to Render
120+
run: |
121+
echo "✅ Backend deployed to Render"
122+
- name: Simulate deployment to Vercel
123+
run: |
124+
echo "✅ Frontend deployed to Vercel"
106125
107126
complete:
108-
name: "🎉 Pipeline Complete"
127+
name: 🎉 All Done
109128
runs-on: ubuntu-latest
110-
needs: [formatting, backend, frontend]
111-
if: ${{ always() }}
129+
needs: deploy
112130
steps:
113131
- name: Final status
114132
run: echo "✅ CI/CD pipeline finished successfully."

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Budget-Management-Backend-API/
172172
├── .prettierrc # Prettier configuration for code formatting
173173
├── LICENSE # License information
174174
├── README.md # Documentation for the project
175-
├── app.test.js # Main test file for application
175+
├── __tests__/app.test.js # Main test file for application
176176
├── cli.js # CLI tool for interacting with the backend
177177
├── docker-compose.yml # Docker Compose configuration
178178
├── Dockerfile # Dockerfile for containerizing the application

__tests__/app.test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
jest.mock('mongoose', () => ({
2+
connect: jest.fn().mockResolvedValue(),
3+
}));
4+
5+
jest.mock('../services/redisService', () => ({}));
6+
7+
jest.mock('../apache-kafka/kafkaService', () => ({
8+
connectToKafka: jest.fn().mockResolvedValue(),
9+
}));
10+
11+
jest.mock('../services/rabbitMQService', () => ({
12+
connectToRabbitMQ: jest.fn().mockResolvedValue(),
13+
}));
14+
15+
jest.mock('../services/dataSeeder', () => jest.fn().mockResolvedValue());
16+
17+
jest.mock('../grpcServer', () => jest.fn());
18+
19+
jest.mock('morgan', () => () => (req, res, next) => next());
20+
21+
jest.mock('serve-favicon', () => () => (req, res, next) => next());
22+
23+
jest.mock('../routes', () => {
24+
const express = require('express');
25+
const router = express.Router();
26+
router.get('/ping', (_req, res) => res.json({ pong: true }));
27+
return router;
28+
});
29+
30+
jest.mock('swagger-ui-express', () => ({
31+
serve: jest.fn((req, res, next) => next()),
32+
setup: jest.fn(() => {
33+
return (_req, res) => res.send('<html>SWAGGER UI</html>');
34+
}),
35+
}));
36+
37+
const request = require('supertest');
38+
const path = require('path');
39+
const fs = require('fs');
40+
const app = require('../index');
41+
42+
describe('Express app', () => {
43+
beforeAll(() => {
44+
// create views/home.html
45+
const viewsDir = path.join(__dirname, '../views');
46+
fs.mkdirSync(viewsDir, { recursive: true });
47+
fs.writeFileSync(path.join(viewsDir, 'home.html'), '<h1>Home Page</h1>');
48+
49+
// create public/test.txt for static serving
50+
const publicDir = path.join(__dirname, '../public');
51+
fs.mkdirSync(publicDir, { recursive: true });
52+
fs.writeFileSync(path.join(publicDir, 'test.txt'), 'Hello, Static!');
53+
});
54+
55+
afterAll(() => {
56+
// clean up views/home.html
57+
const htmlPath = path.join(__dirname, '../views/home.html');
58+
if (fs.existsSync(htmlPath)) fs.unlinkSync(htmlPath);
59+
60+
// clean up public/test.txt
61+
const txtPath = path.join(__dirname, '../public/test.txt');
62+
if (fs.existsSync(txtPath)) fs.unlinkSync(txtPath);
63+
});
64+
65+
it('serves the homepage at GET /', async () => {
66+
const res = await request(app).get('/');
67+
expect(res.status).toBe(200);
68+
expect(res.text).toContain('<h1>Home Page</h1>');
69+
});
70+
71+
it('serves Swagger UI at GET /docs', async () => {
72+
const res = await request(app).get('/docs');
73+
expect(res.status).toBe(200);
74+
expect(res.text).toContain('SWAGGER UI');
75+
});
76+
77+
it('responds on ping route under /api', async () => {
78+
const res = await request(app).get('/api/ping');
79+
expect(res.status).toBe(200);
80+
expect(res.body).toEqual({ pong: true });
81+
});
82+
83+
it('includes CORS headers on API responses', async () => {
84+
const res = await request(app).get('/api/ping');
85+
expect(res.headers['access-control-allow-origin']).toBe('*');
86+
});
87+
88+
it('serves static files from public/', async () => {
89+
const res = await request(app).get('/test.txt');
90+
expect(res.status).toBe(200);
91+
expect(res.text).toBe('Hello, Static!');
92+
expect(res.headers['content-type']).toMatch(/text\/plain/);
93+
});
94+
95+
it('404s on unknown non-API route', async () => {
96+
const res = await request(app).get('/does-not-exist');
97+
expect(res.status).toBe(404);
98+
});
99+
100+
it('404s on nonexistent /api path', async () => {
101+
const res = await request(app).get('/api/does-not-exist');
102+
expect(res.status).toBe(404);
103+
});
104+
});

0 commit comments

Comments
 (0)