690419:1411 feat: update CI/CD to use SSH key authentication #05
This commit is contained in:
+15
-15
@@ -32,32 +32,32 @@ fi
|
||||
|
||||
cd "$SOURCE_DIR"
|
||||
|
||||
# [1/3] Build images
|
||||
echo "[1/3] Building Docker images..."
|
||||
echo " Building backend..."
|
||||
docker build -f backend/Dockerfile -t lcbp3-backend:latest . || {
|
||||
echo "✗ Backend build failed!"
|
||||
exit 1
|
||||
}
|
||||
# เปิด BuildKit เพื่อ layer cache และ parallel build
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
# [1/3] Build images (parallel)
|
||||
echo "[1/3] Building Docker images (parallel)..."
|
||||
|
||||
docker build -f backend/Dockerfile -t lcbp3-backend:latest . &
|
||||
BACKEND_PID=$!
|
||||
|
||||
echo " Building frontend (API: $API_URL)..."
|
||||
docker build -f frontend/Dockerfile \
|
||||
--build-arg NEXT_PUBLIC_API_URL="$API_URL" \
|
||||
--build-arg AUTH_URL="$AUTH_URL" \
|
||||
-t lcbp3-frontend:latest . || {
|
||||
echo "✗ Frontend build failed!"
|
||||
exit 1
|
||||
}
|
||||
-t lcbp3-frontend:latest . &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
wait $BACKEND_PID || { echo "✗ Backend build failed!"; exit 1; }
|
||||
wait $FRONTEND_PID || { echo "✗ Frontend build failed!"; exit 1; }
|
||||
echo "✓ Images built"
|
||||
|
||||
# [2/3] Start / restart stack with new images
|
||||
echo "[2/3] Starting application stack..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
echo "✓ Stack started"
|
||||
|
||||
# [3/3] Health check
|
||||
echo "[3/3] Waiting for backend to be healthy..."
|
||||
sleep 10
|
||||
for i in $(seq 1 30); do
|
||||
if docker exec backend curl -sf http://localhost:3000/health > /dev/null 2>&1 || \
|
||||
docker exec backend curl -sf http://localhost:3000/ping > /dev/null 2>&1; then
|
||||
@@ -66,7 +66,7 @@ for i in $(seq 1 30); do
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "✗ Backend health check failed after 60s"
|
||||
docker compose -f "$COMPOSE_FILE" logs backend --tail=50
|
||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" logs backend --tail=50
|
||||
exit 1
|
||||
fi
|
||||
echo " Waiting... ($i/30)"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Performance Verification Scripts
|
||||
|
||||
## T048: ADR-021 Workflow Transition P95 SLA
|
||||
|
||||
**Target:** `POST /workflow-engine/instances/:id/transition` (พร้อม file ≤ 10MB) ต้องตอบสนองภายใน **P95 ≤ 5 วินาที** (Clarify Q4)
|
||||
|
||||
ครอบคลุม: ClamAV scan + Redlock acquire + DB transaction
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. ติดตั้ง k6
|
||||
|
||||
```powershell
|
||||
# Windows (Chocolatey)
|
||||
choco install k6
|
||||
|
||||
# หรือ Download binary จาก https://k6.io/docs/getting-started/installation
|
||||
```
|
||||
|
||||
### 2. เตรียมข้อมูลทดสอบ
|
||||
|
||||
ต้องมี workflow instance และ attachment ที่ commit แล้วพร้อมใช้:
|
||||
|
||||
```bash
|
||||
# ก. Login เพื่อเอา JWT token + user_id
|
||||
curl -X POST http://localhost:3001/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"xxx"}'
|
||||
|
||||
# ข. อัปโหลดไฟล์ 5MB PDF (Two-Phase: upload → commit)
|
||||
curl -X POST http://localhost:3001/api/files/upload \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "file=@test-5mb.pdf"
|
||||
# จะได้ publicId + tempId
|
||||
|
||||
curl -X POST http://localhost:3001/api/files/commit \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tempId":"<temp>"}'
|
||||
# จะได้ publicId ที่ is_temporary=false
|
||||
|
||||
# ค. สร้าง / หา workflow instance ในสถานะ PENDING_REVIEW หรือ PENDING_APPROVAL
|
||||
# (ดู quickstart.md Step 3-4)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Run Test
|
||||
|
||||
```powershell
|
||||
# กำหนด env vars
|
||||
$env:BASE_URL = "http://localhost:3001"
|
||||
$env:USERNAME = "admin"
|
||||
$env:PASSWORD = "xxx"
|
||||
$env:INSTANCE_ID = "<workflow instance UUID>"
|
||||
$env:ATTACHMENT_UUID = "<committed attachment publicId>"
|
||||
|
||||
# รัน smoke test (1 VU × 10 iterations)
|
||||
k6 run scripts/perf/workflow-transition.k6.js
|
||||
```
|
||||
|
||||
### ผลลัพธ์ที่คาดหวัง
|
||||
|
||||
```
|
||||
✓ transition_duration_ms.............: p(95) < 5000 ✓
|
||||
✓ http_req_failed.....................: rate < 0.01 ✓
|
||||
|
||||
running (00m12.3s), 0/1 VUs, 10 complete and 0 interrupted iterations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual Fallback (ถ้าไม่มี k6)
|
||||
|
||||
รัน 10 ครั้งแล้วคำนวณ P95 เอง:
|
||||
|
||||
```bash
|
||||
for i in {1..10}; do
|
||||
curl -X POST "http://localhost:3001/api/workflow-engine/instances/$INSTANCE_ID/transition" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Idempotency-Key: $(uuidgen)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"action\":\"APPROVE\",\"attachmentPublicIds\":[\"$ATTACHMENT_UUID\"]}" \
|
||||
-w "Request $i: %{time_total}s (HTTP %{http_code})\n" \
|
||||
-o /dev/null -s
|
||||
done
|
||||
```
|
||||
|
||||
ผลที่ยอมรับได้:
|
||||
- ทุก request < 5.0s (P95 ≤ 5s)
|
||||
- HTTP 2xx สำหรับ request แรก และ cached 200 สำหรับ request ถัดไปที่ใช้ key เดิม
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| อาการ | สาเหตุที่น่าจะเป็น | แก้ |
|
||||
|---|---|---|
|
||||
| P95 > 5s | ClamAV scan ช้า / Redis ล่าช้า | ตรวจ `docker stats` — ClamAV RAM, Redis connection pool |
|
||||
| HTTP 409 | Instance อยู่ใน Terminal state | เตรียม instance ใหม่ใน PENDING_REVIEW |
|
||||
| HTTP 503 | Redis ล่ม / Redlock timeout | ตรวจ Redis container health |
|
||||
| HTTP 400 | Idempotency-Key missing | ตรวจ header ใน k6 script |
|
||||
| HTTP 403 | User ไม่มีสิทธิ์ | ใช้ assigned handler หรือ superadmin |
|
||||
@@ -0,0 +1,128 @@
|
||||
// ADR-021 T048: Performance Verification — P95 ≤ 5s for POST /workflow-engine/instances/:id/transition
|
||||
// Clarify Q4: file ≤ 10MB (รวม ClamAV scan + Redlock + DB transaction)
|
||||
//
|
||||
// ใช้งาน:
|
||||
// k6 run --env BASE_URL=http://localhost:3001 \
|
||||
// --env USERNAME=admin --env PASSWORD=xxx \
|
||||
// --env INSTANCE_ID=<uuid> \
|
||||
// --env ATTACHMENT_UUID=<uuid> \
|
||||
// scripts/perf/workflow-transition.k6.js
|
||||
//
|
||||
// Prerequisite:
|
||||
// 1. มี workflow instance ในสถานะ PENDING_REVIEW หรือ PENDING_APPROVAL
|
||||
// 2. มีไฟล์แนบขนาด 5-10MB ที่ uploaded_by_user_id = <USERNAME>'s user_id
|
||||
// และ is_temporary = false (commit แล้วผ่าน Two-Phase)
|
||||
// 3. User มีสิทธิ์เป็น assigned handler หรือ superadmin
|
||||
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';
|
||||
import { Trend } from 'k6/metrics';
|
||||
|
||||
// Custom metric เฉพาะ transition endpoint (ไม่รวม login/upload)
|
||||
const transitionDuration = new Trend('transition_duration_ms', true);
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
// Smoke test — 1 VU, 10 iterations = sample 10 transitions
|
||||
smoke: {
|
||||
executor: 'per-vu-iterations',
|
||||
vus: 1,
|
||||
iterations: 10,
|
||||
maxDuration: '2m',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
// ADR-021 Clarify Q4: P95 ≤ 5000ms
|
||||
'transition_duration_ms': ['p(95) < 5000'],
|
||||
'http_req_failed': ['rate < 0.01'], // < 1% failure rate
|
||||
},
|
||||
};
|
||||
|
||||
// ==============================================================
|
||||
// Setup — authenticate ครั้งเดียว
|
||||
// ==============================================================
|
||||
export function setup() {
|
||||
const baseUrl = __ENV.BASE_URL || 'http://localhost:3001';
|
||||
const username = __ENV.USERNAME;
|
||||
const password = __ENV.PASSWORD;
|
||||
const instanceId = __ENV.INSTANCE_ID;
|
||||
const attachmentUuid = __ENV.ATTACHMENT_UUID;
|
||||
|
||||
if (!username || !password || !instanceId || !attachmentUuid) {
|
||||
throw new Error(
|
||||
'Missing env vars. Required: USERNAME, PASSWORD, INSTANCE_ID, ATTACHMENT_UUID'
|
||||
);
|
||||
}
|
||||
|
||||
const loginRes = http.post(
|
||||
`${baseUrl}/api/auth/login`,
|
||||
JSON.stringify({ username, password }),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
check(loginRes, {
|
||||
'login successful': (r) => r.status === 200 || r.status === 201,
|
||||
});
|
||||
|
||||
const body = loginRes.json();
|
||||
const token = body.accessToken || body.data?.accessToken || body.token;
|
||||
if (!token) {
|
||||
throw new Error(`Cannot extract token. Response: ${loginRes.body}`);
|
||||
}
|
||||
|
||||
return { baseUrl, token, instanceId, attachmentUuid };
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// Default scenario — POST transition พร้อมไฟล์แนบ 1 ไฟล์
|
||||
// ==============================================================
|
||||
export default function (data) {
|
||||
const { baseUrl, token, instanceId, attachmentUuid } = data;
|
||||
|
||||
// ป้องกัน idempotency cache hit — สร้าง key ใหม่ทุกครั้ง
|
||||
const idempotencyKey = uuidv4();
|
||||
|
||||
const payload = JSON.stringify({
|
||||
action: 'APPROVE',
|
||||
comment: `k6 perf test iter ${__ITER}`,
|
||||
attachmentPublicIds: [attachmentUuid],
|
||||
});
|
||||
|
||||
const params = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Idempotency-Key': idempotencyKey,
|
||||
},
|
||||
tags: { name: 'workflow-transition' },
|
||||
};
|
||||
|
||||
const start = Date.now();
|
||||
const res = http.post(
|
||||
`${baseUrl}/api/workflow-engine/instances/${instanceId}/transition`,
|
||||
payload,
|
||||
params
|
||||
);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
transitionDuration.add(duration);
|
||||
|
||||
check(res, {
|
||||
'status is 2xx': (r) => r.status >= 200 && r.status < 300,
|
||||
'duration < 5s (P95 SLA)': () => duration < 5000,
|
||||
});
|
||||
|
||||
if (res.status >= 400) {
|
||||
console.error(`Iteration ${__ITER} failed: ${res.status} — ${res.body}`);
|
||||
}
|
||||
|
||||
sleep(1); // เว้นระหว่างแต่ละ iteration ให้ worker breathe
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// Teardown — รายงานผลสรุป (k6 auto-report ให้แล้ว ใช้นี้เมื่อต้องการ custom)
|
||||
// ==============================================================
|
||||
export function teardown(data) {
|
||||
console.log(`Perf test done. Instance: ${data.instanceId}`);
|
||||
}
|
||||
Reference in New Issue
Block a user