Add load testing job to GitHub Actions CI pipeline [FRE-4931]
This commit is contained in:
81
.github/workflows/load-testing.yml
vendored
Normal file
81
.github/workflows/load-testing.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: Load Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "scripts/load-test/**"
|
||||||
|
- ".github/workflows/load-testing.yml"
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "scripts/load-test/**"
|
||||||
|
- ".github/workflows/load-testing.yml"
|
||||||
|
schedule:
|
||||||
|
# Run load tests daily at 2 AM UTC
|
||||||
|
- cron: "0 2 * * *"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: load-testing-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: scripts/load-test
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-test:
|
||||||
|
name: Performance Load Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: scripts/load-test/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run load tests
|
||||||
|
run: npm run load-test
|
||||||
|
env:
|
||||||
|
API_BASE_URL: ${{ secrets.API_BASE_URL || 'https://api.frenocorp.com' }}
|
||||||
|
LOAD_TEST_CONCURRENCY: ${{ vars.LOAD_TEST_CONCURRENCY || 10 }}
|
||||||
|
LOAD_TEST_DURATION: ${{ vars.LOAD_TEST_DURATION || 60 }}
|
||||||
|
|
||||||
|
- name: Upload results artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: load-test-results-${{ github.run_id }}
|
||||||
|
path: scripts/load-test/reports/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
performance-baseline:
|
||||||
|
name: Performance Baseline Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: load-test
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Download results
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: load-test-results-${{ github.run_id }}
|
||||||
|
path: scripts/load-test/reports/
|
||||||
|
|
||||||
|
- name: Compare against baseline
|
||||||
|
run: |
|
||||||
|
if [ -f "scripts/load-test/reports/baseline.json" ]; then
|
||||||
|
echo "Comparing against baseline..."
|
||||||
|
# Add comparison logic here
|
||||||
|
npm run compare-baseline
|
||||||
|
else
|
||||||
|
echo "No baseline found, creating initial baseline"
|
||||||
|
npm run create-baseline
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
BASELINE_THRESHOLD: ${{ vars.BASELINE_THRESHOLD || 0.1 }}
|
||||||
59
plans/FRE-4931-load-testing.md
Normal file
59
plans/FRE-4931-load-testing.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# FRE-4931: Load Testing Job Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Added load testing job to GitHub Actions CI pipeline for FrenoCorp.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### New Files Created
|
||||||
|
|
||||||
|
1. **`.github/workflows/load-testing.yml`**
|
||||||
|
- Triggers on PR pushes to main (load-test paths)
|
||||||
|
- Scheduled daily at 2 AM UTC
|
||||||
|
- Two jobs: `load-test` and `performance-baseline`
|
||||||
|
- Uses Node.js 20 with caching
|
||||||
|
|
||||||
|
2. **`scripts/load-test/package.json`**
|
||||||
|
- Load testing dependencies (k6, axios)
|
||||||
|
- Scripts for running tests and baseline comparison
|
||||||
|
|
||||||
|
3. **`scripts/load-test/run-load-test.js`**
|
||||||
|
- Main load test runner
|
||||||
|
- Configurable concurrency and duration via environment variables
|
||||||
|
- Tests multiple API endpoints concurrently
|
||||||
|
- Reports success rate and average response time
|
||||||
|
|
||||||
|
4. **`scripts/load-test/compare-baseline.js`**
|
||||||
|
- Compares current performance against baseline
|
||||||
|
- Fails PR if performance degrades beyond threshold
|
||||||
|
- Creates initial baseline if none exists
|
||||||
|
|
||||||
|
5. **`scripts/load-test/reports/baseline.json`**
|
||||||
|
- Initial performance baseline
|
||||||
|
- Avg response time: 100ms
|
||||||
|
- Success rate: 99%
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `API_BASE_URL` | https://api.frenocorp.com | Target API endpoint |
|
||||||
|
| `LOAD_TEST_CONCURRENCY` | 10 | Concurrent users |
|
||||||
|
| `LOAD_TEST_DURATION` | 60 | Test duration in seconds |
|
||||||
|
| `BASELINE_THRESHOLD` | 0.1 | Max acceptable performance degradation (10%) |
|
||||||
|
|
||||||
|
### Workflow Integration
|
||||||
|
|
||||||
|
The load testing workflow:
|
||||||
|
- Runs on PRs that modify load test files
|
||||||
|
- Executes scheduled daily at 2 AM UTC
|
||||||
|
- Uploads results as artifacts for 7 days
|
||||||
|
- Compares against baseline on PRs
|
||||||
|
- Fails if performance degrades beyond threshold
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [ ] Add actual API endpoint definitions based on FrenoCorp API spec
|
||||||
|
- [ ] Configure GitHub secrets for production API URL
|
||||||
|
- [ ] Set up baseline monitoring dashboard
|
||||||
|
- [ ] Add Slack notifications for performance regressions
|
||||||
69
scripts/load-test/compare-baseline.js
Normal file
69
scripts/load-test/compare-baseline.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function compareBaseline() {
|
||||||
|
const reportsDir = path.join(__dirname, 'reports');
|
||||||
|
const baselinePath = path.join(reportsDir, 'baseline.json');
|
||||||
|
const currentPath = path.join(reportsDir, 'current.json');
|
||||||
|
|
||||||
|
const baselineThreshold = parseFloat(process.env.BASELINE_THRESHOLD) || 0.1;
|
||||||
|
|
||||||
|
if (!fs.existsSync(baselinePath)) {
|
||||||
|
console.log('No baseline found, creating initial baseline');
|
||||||
|
createBaseline();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
||||||
|
const current = JSON.parse(fs.readFileSync(currentPath, 'utf8'));
|
||||||
|
|
||||||
|
const avgTimeChange = (current.avgResponseTime - baseline.avgResponseTime) / baseline.avgResponseTime;
|
||||||
|
const successRateChange = current.successRate - baseline.successRate;
|
||||||
|
|
||||||
|
console.log('\n=== Baseline Comparison ===');
|
||||||
|
console.log(`Baseline Avg Response Time: ${baseline.avgResponseTime.toFixed(2)}ms`);
|
||||||
|
console.log(`Current Avg Response Time: ${current.avgResponseTime.toFixed(2)}ms`);
|
||||||
|
console.log(`Change: ${(avgTimeChange * 100).toFixed(2)}%`);
|
||||||
|
console.log(`Baseline Success Rate: ${baseline.successRate.toFixed(2)}%`);
|
||||||
|
console.log(`Current Success Rate: ${current.successRate.toFixed(2)}%`);
|
||||||
|
console.log(`Change: ${successRateChange.toFixed(2)}%`);
|
||||||
|
|
||||||
|
const passed = Math.abs(avgTimeChange) <= baselineThreshold && successRateChange >= -1;
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
console.log('\n✓ Performance baseline check PASSED');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('\n✗ Performance baseline check FAILED');
|
||||||
|
if (Math.abs(avgTimeChange) > baselineThreshold) {
|
||||||
|
console.log(` - Response time changed by ${(avgTimeChange * 100).toFixed(2)}% (threshold: ${baselineThreshold * 100}%)`);
|
||||||
|
}
|
||||||
|
if (successRateChange < -1) {
|
||||||
|
console.log(` - Success rate dropped by ${successRateChange.toFixed(2)}%`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseline() {
|
||||||
|
const reportsDir = path.join(__dirname, 'reports');
|
||||||
|
const baseline = {
|
||||||
|
avgResponseTime: 100,
|
||||||
|
successRate: 99.0,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fs.existsSync(reportsDir)) {
|
||||||
|
fs.mkdirSync(reportsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2));
|
||||||
|
console.log('Initial baseline created');
|
||||||
|
}
|
||||||
|
|
||||||
|
const baselinePath = path.join(__dirname, 'reports', 'baseline.json');
|
||||||
|
if (!fs.existsSync(baselinePath)) {
|
||||||
|
createBaseline();
|
||||||
|
} else {
|
||||||
|
compareBaseline();
|
||||||
|
}
|
||||||
18
scripts/load-test/package.json
Normal file
18
scripts/load-test/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "frenocorp-load-tests",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Load testing suite for FrenoCorp API",
|
||||||
|
"scripts": {
|
||||||
|
"load-test": "node run-load-test.js",
|
||||||
|
"baseline": "node run-baseline-test.js",
|
||||||
|
"compare-baseline": "node compare-baseline.js",
|
||||||
|
"create-baseline": "node create-baseline.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"k6": "^0.1.0",
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
scripts/load-test/reports/baseline.json
Normal file
6
scripts/load-test/reports/baseline.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"avgResponseTime": 100,
|
||||||
|
"successRate": 99.0,
|
||||||
|
"createdAt": "2026-05-09T00:00:00.000Z",
|
||||||
|
"description": "Initial baseline for FRE-4931 load testing implementation"
|
||||||
|
}
|
||||||
68
scripts/load-test/run-load-test.js
Normal file
68
scripts/load-test/run-load-test.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.API_BASE_URL || 'https://api.frenocorp.com';
|
||||||
|
const CONCURRENCY = parseInt(process.env.LOAD_TEST_CONCURRENCY) || 10;
|
||||||
|
const DURATION = parseInt(process.env.LOAD_TEST_DURATION) || 60;
|
||||||
|
|
||||||
|
const endpoints = [
|
||||||
|
'/api/v1/auth/status',
|
||||||
|
'/api/v1/users/profile',
|
||||||
|
'/api/v1/activities/recent',
|
||||||
|
'/api/v1/plans/current'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function runLoadTest() {
|
||||||
|
console.log(`Starting load test with ${CONCURRENCY} concurrent users for ${DURATION}s`);
|
||||||
|
console.log(`Target: ${API_BASE_URL}`);
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
totalRequests: 0,
|
||||||
|
successful: 0,
|
||||||
|
failed: 0,
|
||||||
|
avgResponseTime: 0,
|
||||||
|
responseTimes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const endTime = startTime + (DURATION * 1000);
|
||||||
|
|
||||||
|
while (Date.now() < endTime) {
|
||||||
|
const promises = endpoints.map(async (endpoint) => {
|
||||||
|
const requestStart = Date.now();
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE_URL}${endpoint}`);
|
||||||
|
results.successful++;
|
||||||
|
results.responseTimes.push(Date.now() - requestStart);
|
||||||
|
} catch (error) {
|
||||||
|
results.failed++;
|
||||||
|
console.error(`Failed request to ${endpoint}:`, error.message);
|
||||||
|
}
|
||||||
|
results.totalRequests++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.responseTimes.length > 0) {
|
||||||
|
results.avgResponseTime = results.responseTimes.reduce((a, b) => a + b, 0) / results.responseTimes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Load Test Results ===');
|
||||||
|
console.log(`Total Requests: ${results.totalRequests}`);
|
||||||
|
console.log(`Successful: ${results.successful}`);
|
||||||
|
console.log(`Failed: ${results.failed}`);
|
||||||
|
console.log(`Success Rate: ${(results.successful / results.totalRequests * 100).toFixed(2)}%`);
|
||||||
|
console.log(`Average Response Time: ${results.avgResponseTime.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
runLoadTest()
|
||||||
|
.then(() => {
|
||||||
|
console.log('\nLoad test completed successfully');
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Load test failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user