feat(ops): add first deployment runbook tooling

This commit is contained in:
2026-03-08 22:44:36 +04:00
parent c5c356f2b2
commit c6a9ade586
10 changed files with 547 additions and 1 deletions

124
scripts/ops/deploy-smoke.ts Normal file
View File

@@ -0,0 +1,124 @@
function requireEnv(name: string): string {
const value = process.env[name]?.trim()
if (!value) {
throw new Error(`${name} is required`)
}
return value
}
function toUrl(base: string, path: string): URL {
const normalizedBase = base.endsWith('/') ? base : `${base}/`
return new URL(path.replace(/^\//, ''), normalizedBase)
}
async function expectJson(url: URL, init: RequestInit, expectedStatus: number): Promise<any> {
const response = await fetch(url, init)
const text = await response.text()
const payload = (text.length > 0 ? JSON.parse(text) : null) as unknown
if (response.status !== expectedStatus) {
throw new Error(
`${url.toString()} expected ${expectedStatus}, received ${response.status}: ${text}`
)
}
return payload
}
async function fetchWebhookInfo(botToken: string): Promise<any> {
const response = await fetch(`https://api.telegram.org/bot${botToken}/getWebhookInfo`)
const payload = (await response.json()) as {
ok?: boolean
result?: unknown
}
if (!response.ok || payload.ok !== true) {
throw new Error(`Telegram getWebhookInfo failed: ${JSON.stringify(payload)}`)
}
return payload.result
}
async function run(): Promise<void> {
const botApiUrl = requireEnv('BOT_API_URL')
const miniAppUrl = requireEnv('MINI_APP_URL')
const health = await expectJson(toUrl(botApiUrl, '/healthz'), {}, 200)
if (health?.ok !== true) {
throw new Error('Bot health check returned unexpected payload')
}
await expectJson(
toUrl(botApiUrl, '/api/miniapp/session'),
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({})
},
400
)
await expectJson(
toUrl(botApiUrl, '/jobs/reminder/utilities'),
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({})
},
401
)
const miniAppResponse = await fetch(miniAppUrl)
const miniAppHtml = await miniAppResponse.text()
if (!miniAppResponse.ok) {
throw new Error(`Mini app root returned ${miniAppResponse.status}`)
}
if (!miniAppHtml.includes('/config.js')) {
throw new Error('Mini app root does not reference runtime config')
}
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN?.trim()
const expectedWebhookUrl = process.env.TELEGRAM_EXPECTED_WEBHOOK_URL?.trim()
if (telegramBotToken && expectedWebhookUrl) {
const webhookInfo = await fetchWebhookInfo(telegramBotToken)
if (webhookInfo.url !== expectedWebhookUrl) {
throw new Error(
`Telegram webhook mismatch: expected ${expectedWebhookUrl}, received ${webhookInfo.url}`
)
}
if (
typeof webhookInfo.last_error_message === 'string' &&
webhookInfo.last_error_message.length > 0
) {
throw new Error(
`Telegram webhook reports last_error_message=${webhookInfo.last_error_message}`
)
}
}
console.log(
JSON.stringify(
{
ok: true,
botApiUrl,
miniAppUrl,
checkedWebhook: telegramBotToken !== undefined && expectedWebhookUrl !== undefined
},
null,
2
)
)
}
run().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
})

View File

@@ -0,0 +1,86 @@
type WebhookCommand = 'info' | 'set' | 'delete'
function requireEnv(name: string): string {
const value = process.env[name]?.trim()
if (!value) {
throw new Error(`${name} is required`)
}
return value
}
async function telegramRequest(
botToken: string,
method: string,
body?: URLSearchParams
): Promise<any> {
const response = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
method: body ? 'POST' : 'GET',
body
})
const payload = (await response.json()) as {
ok?: boolean
result?: unknown
}
if (!response.ok || payload.ok !== true) {
throw new Error(`Telegram ${method} failed: ${JSON.stringify(payload)}`)
}
return payload.result
}
async function run(): Promise<void> {
const command = (process.argv[2] ?? 'info') as WebhookCommand
const botToken = requireEnv('TELEGRAM_BOT_TOKEN')
switch (command) {
case 'info': {
const result = await telegramRequest(botToken, 'getWebhookInfo')
console.log(JSON.stringify(result, null, 2))
return
}
case 'set': {
const params = new URLSearchParams({
url: requireEnv('TELEGRAM_WEBHOOK_URL')
})
const secretToken = process.env.TELEGRAM_WEBHOOK_SECRET?.trim()
if (secretToken) {
params.set('secret_token', secretToken)
}
const maxConnections = process.env.TELEGRAM_MAX_CONNECTIONS?.trim()
if (maxConnections) {
params.set('max_connections', maxConnections)
}
const dropPendingUpdates = process.env.TELEGRAM_DROP_PENDING_UPDATES?.trim()
if (dropPendingUpdates) {
params.set('drop_pending_updates', dropPendingUpdates)
}
const result = await telegramRequest(botToken, 'setWebhook', params)
console.log(JSON.stringify({ ok: true, result }, null, 2))
return
}
case 'delete': {
const params = new URLSearchParams()
const dropPendingUpdates = process.env.TELEGRAM_DROP_PENDING_UPDATES?.trim()
if (dropPendingUpdates) {
params.set('drop_pending_updates', dropPendingUpdates)
}
const result = await telegramRequest(botToken, 'deleteWebhook', params)
console.log(JSON.stringify({ ok: true, result }, null, 2))
return
}
default:
throw new Error(`Unsupported command: ${command}`)
}
}
run().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
})