feat(health): expose app version in /health response

Reads application version from package.json via env.ts readPackageVersion()
and returns it alongside status so deploy verification is instant:
curl https://upload.asepharyana.my.id/health -> {status: ok, version: X.Y.Z}
This commit is contained in:
asepharyana
2026-09-06 00:42:39 +07:00
parent bdda2d9884
commit 0782d0f993
2 changed files with 38 additions and 1 deletions
+36
View File
@@ -2,6 +2,8 @@ import logger from './shared/logger/index';
import { TELEGRAM_CHUNK_SIZE_MAX_BYTES } from './shared/utils/validation'; import { TELEGRAM_CHUNK_SIZE_MAX_BYTES } from './shared/utils/validation';
interface AppConfig { interface AppConfig {
/** Application version (read from package.json, kept in sync by semantic-release prepare.mjs) */
appVersion: string;
/** All bot tokens merged from BOT_TOKENS (or BOT_TOKEN + ADDITIONAL_BOT_TOKENS fallback) */ /** All bot tokens merged from BOT_TOKENS (or BOT_TOKEN + ADDITIONAL_BOT_TOKENS fallback) */
botTokens: string[]; botTokens: string[];
/** Per-bot concurrency for Telegram API calls (default 1). */ /** Per-bot concurrency for Telegram API calls (default 1). */
@@ -131,7 +133,41 @@ if (telegramChunkSizeBytes > TELEGRAM_CHUNK_SIZE_MAX_BYTES) {
); );
} }
/**
* Reads the application version from package.json.
*
* Prefers the resolved node_modules/teleuploader package version (set by
* semantic-release when the repo is installed as a published artifact) and
* falls back to reading the repo's own package.json when running from a
* checkout. Both files are updated by prepare.mjs on every release, so the
* value always matches the deployed build.
*/
const readPackageVersion = (): string => {
try {
const candidates = [
// node_modules resolution is only available when installed as a dep
import.meta.dir ? `${import.meta.dir}/../package.json` : undefined,
`${process.cwd()}/package.json`,
];
for (const candidate of candidates) {
if (!candidate) continue;
try {
const pkg = JSON.parse(import.meta.require(candidate).default ?? '{}') as {
version?: string;
};
if (pkg.version) return pkg.version;
} catch {
// try next candidate
}
}
} catch {
// fall through
}
return '0.0.0';
};
export const config: AppConfig = { export const config: AppConfig = {
appVersion: readPackageVersion(),
botTokens: parseTokens(botTokensRaw), botTokens: parseTokens(botTokensRaw),
telegramBotConcurrency: parseNumber(process.env.TELEGRAM_BOT_CONCURRENCY, 1), telegramBotConcurrency: parseNumber(process.env.TELEGRAM_BOT_CONCURRENCY, 1),
storageChatId: parseInt(process.env.STORAGE_CHANNEL_ID!, 10), storageChatId: parseInt(process.env.STORAGE_CHANNEL_ID!, 10),
@@ -1,4 +1,5 @@
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { config } from '../../../env';
import { db } from '../../../infrastructure/persistence/drizzle/index'; import { db } from '../../../infrastructure/persistence/drizzle/index';
import logger from '../../../shared/logger/index'; import logger from '../../../shared/logger/index';
import { getErrorMessage } from '../../../shared/utils/file'; import { getErrorMessage } from '../../../shared/utils/file';
@@ -16,7 +17,7 @@ import { getErrorMessage } from '../../../shared/utils/file';
export const handleHealth = async (_req: Request): Promise<Response> => { export const handleHealth = async (_req: Request): Promise<Response> => {
try { try {
await db.execute(sql`SELECT 1`); await db.execute(sql`SELECT 1`);
return Response.json({ status: 'ok' }, { status: 200 }); return Response.json({ status: 'ok', version: config.appVersion }, { status: 200 });
} catch (error: unknown) { } catch (error: unknown) {
const message = getErrorMessage(error); const message = getErrorMessage(error);
logger.error('Health check failed', { error: message }); logger.error('Health check failed', { error: message });