add persistent readiness health check

This commit is contained in:
2026-09-06 18:37:11 +03:00
parent b10546b81a
commit 11685867e2
4 changed files with 40 additions and 2 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
cleanup() { docker rm -f "$cid" >/dev/null 2>&1 || true; } cleanup() { docker rm -f "$cid" >/dev/null 2>&1 || true; }
trap cleanup EXIT trap cleanup EXIT
for attempt in $(seq 1 30); do for attempt in $(seq 1 30); do
if curl --fail --silent http://127.0.0.1:18080/api/health; then exit 0; fi if curl --fail --silent http://127.0.0.1:18080/api/ready; then exit 0; fi
sleep 1 sleep 1
done done
echo 'Container health endpoint did not become ready' >&2 echo 'Container health endpoint did not become ready' >&2
+7 -1
View File
@@ -2085,4 +2085,10 @@ Docker runtime image’ında backend’in kullandığı ortak `src/` klasörün
### 2026-09-06 · CI kalite kapıları ### 2026-09-06 · CI kalite kapıları
GitHub Actions iş akışı eklendi. Her push ve pull request’te test/üretim derlemesi; ayrıca Docker image build’i, nginx yapılandırma testi ve `/api/health` çalışma zamanı kontrolü otomatik çalışacak. Bu iş akışı gerçek production rollout kanıtının yerine geçmez. GitHub Actions iş akışı eklendi. Her push ve pull request’te test/üretim derlemesi; ayrıca Docker image build’i, nginx yapılandırma testi ve `/api/ready` çalışma zamanı kontrolü otomatik çalışacak. Bu iş akışı gerçek production rollout kanıtının yerine geçmez.
### 2026-09-06 · Production readiness endpoint’i
`/api/ready` eklendi. `/api/health` süreç canlılığını korurken readiness kontrolü kullanıcı ve ortak veri dizinlerini gerektiğinde oluşturup okuma/yazma erişimini doğruluyor; başarısızlıkta HTTP 503 ve hangi kontrolün kaldığını döndürüyor. CI container kontrolü artık bu endpoint’i kullanıyor.
Doğrulama: HTTP readiness testi geçti; toplam test ve build doğrulaması CI’da çalışacak.
+24
View File
@@ -75,6 +75,26 @@ const PORT = process.env.PORT || 3001;
const ILAN_BASE = 'https://www.ilan.gov.tr'; const ILAN_BASE = 'https://www.ilan.gov.tr';
const CORS_ORIGINS = String(process.env.CORS_ORIGINS || '').split(',').map(value => value.trim()).filter(Boolean); const CORS_ORIGINS = String(process.env.CORS_ORIGINS || '').split(',').map(value => value.trim()).filter(Boolean);
function readiness() {
const directories = {
userData: process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname, 'data'),
sharedData: process.env.IHALEIST_SHARED_DATA_DIR || path.join(__dirname, 'data'),
};
const checks = {};
let ready = true;
for (const [name, directory] of Object.entries(directories)) {
try {
fs.mkdirSync(directory, { recursive: true });
fs.accessSync(directory, fs.constants.R_OK | fs.constants.W_OK);
checks[name] = { ok: true };
} catch (error) {
ready = false;
checks[name] = { ok: false, error: error.code || 'EACCES' };
}
}
return { ready, checks };
}
function validateHealthPortfolio(value) { function validateHealthPortfolio(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Firma profili geçersiz.'); if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Firma profili geçersiz.');
const portfolio = value.scrapedCapabilities?.healthPortfolio; const portfolio = value.scrapedCapabilities?.healthPortfolio;
@@ -1700,6 +1720,10 @@ const server = http.createServer(async (req, res) => {
} }
if (url.pathname === '/api/health') return json(res, 200, { ok: true }); if (url.pathname === '/api/health') return json(res, 200, { ok: true });
if (url.pathname === '/api/ready') {
const status = readiness();
return json(res, status.ready ? 200 : 503, { ok: status.ready, ...status });
}
if (url.pathname === '/api/health/sektor') { if (url.pathname === '/api/health/sektor') {
const say = sektorZenginSayi(); const say = sektorZenginSayi();
const genel = (cache.tenders || []).filter((t) => t.sektor === 'Genel').length; const genel = (cache.tenders || []).filter((t) => t.sektor === 'Genel').length;
+8
View File
@@ -47,6 +47,14 @@ test('HTTP preparation requires auth, persists draft and rejects stale revisions
assert.equal((await request('/api/me/tender-state','PUT',data)).status,409); assert.equal((await request('/api/me/tender-state','PUT',data)).status,409);
assert.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{}); assert.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{});
}); });
test('readiness endpoint checks persistent data directories',async()=>{
const response=await request('/api/ready','GET',null,null);
assert.equal(response.status,200);
assert.equal(response.data.ok,true);
assert.equal(response.data.ready,true);
assert.equal(response.data.checks.userData.ok,true);
assert.equal(response.data.checks.sharedData.ok,true);
});
test('HTTP documents and API keys expose empty successful state, not endless load errors',async()=>{ test('HTTP documents and API keys expose empty successful state, not endless load errors',async()=>{
const docs=await request('/api/me/documents');assert.equal(docs.status,200);assert.deepEqual(docs.data.belgeler,[]); const docs=await request('/api/me/documents');assert.equal(docs.status,200);assert.deepEqual(docs.data.belgeler,[]);
const keys=await request('/api/me/apikeys');assert.equal(keys.status,200);assert.deepEqual(keys.data.anahtarlar,[]); const keys=await request('/api/me/apikeys');assert.equal(keys.status,200);assert.deepEqual(keys.data.anahtarlar,[]);