add persistent readiness health check
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,[]);
|
||||||
|
|||||||
Reference in New Issue
Block a user