fix: address CI and PR review feedback

Co-authored-by: claw <stanislavkalishin+claw@gmail.com>
This commit is contained in:
2026-03-30 15:58:58 +02:00
parent 4d8d2fb6cf
commit ed2fbac284
10 changed files with 85 additions and 42 deletions

View File

@@ -29,13 +29,11 @@ jobs:
outputs:
eligible: ${{ steps.detect.outputs.eligible }}
ref: ${{ steps.detect.outputs.ref }}
sha: ${{ steps.detect.outputs.sha }}
steps:
- id: detect
run: |
eligible=false
ref=""
sha=""
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
eligible=true
@@ -47,7 +45,6 @@ jobs:
echo "eligible=$eligible" >> "$GITHUB_OUTPUT"
echo "ref=$ref" >> "$GITHUB_OUTPUT"
echo "sha=${ref}" >> "$GITHUB_OUTPUT"
build-bot:
runs-on: ubuntu-latest
@@ -147,11 +144,14 @@ jobs:
test -n "${{ secrets.VPS_SSH_KEY }}"
- name: Prepare SSH
env:
VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' '${{ secrets.VPS_SSH_KEY }}' > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "$VPS_PORT" "$VPS_HOST" >> ~/.ssh/known_hosts
test -n "$VPS_KNOWN_HOSTS"
printf '%s\n' "$VPS_KNOWN_HOSTS" > ~/.ssh/known_hosts
- name: Upload deploy bundle
run: |
@@ -172,14 +172,6 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Smoke check
env:
BOT_API_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}
MINI_APP_URL: ${{ vars.VPS_MINIAPP_URL || 'https://household.whekin.dev' }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_EXPECTED_WEBHOOK_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}/webhook/telegram
run: bun run ops:deploy:smoke
- name: Set Telegram webhook
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
@@ -192,3 +184,11 @@ jobs:
fi
bun run ops:telegram:webhook set
- name: Smoke check
env:
BOT_API_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}
MINI_APP_URL: ${{ vars.VPS_MINIAPP_URL || 'https://household.whekin.dev' }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_EXPECTED_WEBHOOK_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}/webhook/telegram
run: bun run ops:deploy:smoke

View File

@@ -141,7 +141,7 @@ export async function createBotRuntimeApp(): Promise<BotRuntimeApp> {
: null
const scheduledDispatchScheduler =
runtime.scheduledDispatch &&
(runtime.scheduledDispatch.provider === 'self-hosted' || runtime.schedulerSharedSecret)
(runtime.scheduledDispatch.provider === 'aws-eventbridge' || runtime.schedulerSharedSecret)
? runtime.scheduledDispatch.provider === 'gcp-cloud-tasks'
? createGcpScheduledDispatchScheduler({
projectId: runtime.scheduledDispatch.projectId,

View File

@@ -248,8 +248,14 @@ export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRu
runtime.schedulerSharedSecret = schedulerSharedSecret
}
if (scheduledDispatch !== undefined) {
if (scheduledDispatch.provider === 'self-hosted' && schedulerSharedSecret === undefined) {
throw new Error('Self-hosted scheduled dispatch requires SCHEDULER_SHARED_SECRET')
if (
(scheduledDispatch.provider === 'self-hosted' ||
scheduledDispatch.provider === 'gcp-cloud-tasks') &&
schedulerSharedSecret === undefined
) {
throw new Error(
`${scheduledDispatch.provider} scheduled dispatch requires SCHEDULER_SHARED_SECRET`
)
}
runtime.scheduledDispatch = scheduledDispatch

View File

@@ -20,15 +20,15 @@ function parsePositiveInteger(name: string, fallback: number): number {
return parsed
}
async function runOnce() {
const baseUrl = requireEnv('BOT_INTERNAL_BASE_URL').replace(/\/$/, '')
const schedulerSecret = requireEnv('SCHEDULER_SHARED_SECRET')
const dueScanLimit = parsePositiveInteger('SCHEDULER_DUE_SCAN_LIMIT', 25)
const response = await fetch(`${baseUrl}/jobs/dispatch-due?limit=${dueScanLimit}`, {
async function runOnce(input: {
baseUrl: string
schedulerSecret: string
dueScanLimit: number
}) {
const response = await fetch(`${input.baseUrl}/jobs/dispatch-due?limit=${input.dueScanLimit}`, {
method: 'POST',
headers: {
'x-household-scheduler-secret': schedulerSecret
'x-household-scheduler-secret': input.schedulerSecret
}
})
@@ -51,6 +51,11 @@ async function runOnce() {
async function main() {
const intervalMs = parsePositiveInteger('SCHEDULER_POLL_INTERVAL_MS', 60_000)
const runConfig = {
baseUrl: requireEnv('BOT_INTERNAL_BASE_URL').replace(/\/$/, ''),
schedulerSecret: requireEnv('SCHEDULER_SHARED_SECRET'),
dueScanLimit: parsePositiveInteger('SCHEDULER_DUE_SCAN_LIMIT', 25)
}
let stopping = false
const stop = () => {
@@ -62,7 +67,7 @@ async function main() {
while (!stopping) {
try {
await runOnce()
await runOnce(runConfig)
} catch (error) {
console.error(
JSON.stringify({

View File

@@ -5,7 +5,11 @@ services:
env_file:
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
healthcheck:
test: ['CMD', 'bun', '-e', "fetch('http://127.0.0.1:' + (process.env.PORT ?? '8080') + '/healthz').then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))"]
test:
- CMD
- bun
- -e
- "fetch('http://127.0.0.1:' + (process.env.PORT ?? '8080') + '/healthz').then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))"
interval: 30s
timeout: 5s
retries: 3
@@ -23,7 +27,9 @@ services:
scheduler:
image: ${BOT_IMAGE:?set BOT_IMAGE}
restart: unless-stopped
command: ['bun', 'apps/bot/dist/scheduler-runner.js']
command:
- bun
- apps/bot/dist/scheduler-runner.js
env_file:
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
environment:
@@ -36,10 +42,13 @@ services:
migrate:
image: ${BOT_IMAGE:?set BOT_IMAGE}
profiles: ['ops']
profiles:
- ops
env_file:
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
command: ['bun', 'packages/db/dist/migrate.js']
command:
- bun
- packages/db/dist/migrate.js
restart: 'no'
caddy:

View File

@@ -1 +1 @@
BOT_API_URL=https://household-bot.whekin.dev
VITE_BOT_API_URL=https://household-bot.whekin.dev

View File

@@ -125,7 +125,6 @@ These can be adjusted later without changing the deployment shape.
6. Validate builds/tests where practical
7. Push branch and open PR
## Runtime Env Files on VPS
Expected files under `/opt/household-bot/env`:
@@ -134,6 +133,7 @@ Expected files under `/opt/household-bot/env`:
- `caddy.env`
Templates live in `deploy/vps/*.env.example`.
- `miniapp.env` should set `VITE_BOT_API_URL` for the frontend build/runtime config.
## GitHub Actions Inputs / Secrets
@@ -145,8 +145,9 @@ Recommended repository variables:
- `VPS_BOT_URL` (default `https://household-bot.whekin.dev`)
- `VPS_MINIAPP_URL` (default `https://household.whekin.dev`)
Required repository secret:
Required repository secrets:
- `VPS_SSH_KEY`
- `VPS_KNOWN_HOSTS`
Optional for webhook sync and smoke verification:
- `TELEGRAM_BOT_TOKEN`

View File

@@ -38,8 +38,8 @@ class NotificationRepositoryStub implements AdHocNotificationRepository {
sentAt: null,
cancelledAt: null,
cancelledByMemberId: null,
createdAt: Temporal.Instant.from('2026-03-23T09:00:00Z'),
updatedAt: Temporal.Instant.from('2026-03-23T09:00:00Z')
createdAt: Temporal.Instant.from('2099-03-23T09:00:00Z'),
updatedAt: Temporal.Instant.from('2099-03-23T09:00:00Z')
}
this.notifications.set(id, record)
return record
@@ -191,7 +191,7 @@ describe('createAdHocNotificationService', () => {
originalRequestText: 'Напомни Георгию завтра',
notificationText: 'пошпынять Георгия о том, позвонил ли он',
timezone: 'Asia/Tbilisi',
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
scheduledFor: Temporal.Instant.from('2099-03-25T08:00:00Z'),
timePrecision: 'date_only_defaulted',
deliveryMode: 'topic'
})
@@ -222,7 +222,7 @@ describe('createAdHocNotificationService', () => {
originalRequestText: 'remind everyone tomorrow',
notificationText: 'pay rent',
timezone: 'Asia/Tbilisi',
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
scheduledFor: Temporal.Instant.from('2099-03-25T08:00:00Z'),
timePrecision: 'date_only_defaulted',
deliveryMode: 'dm_all'
})
@@ -246,7 +246,7 @@ describe('createAdHocNotificationService', () => {
originalRequestText: 'remind tomorrow',
notificationText: 'check rent',
timezone: 'Asia/Tbilisi',
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
scheduledFor: Temporal.Instant.from('2099-03-25T08:00:00Z'),
timePrecision: 'date_only_defaulted',
deliveryMode: 'topic',
friendlyTagAssignee: true
@@ -273,7 +273,7 @@ describe('createAdHocNotificationService', () => {
originalRequestText: 'remind tomorrow',
notificationText: 'call landlord',
timezone: 'Asia/Tbilisi',
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
scheduledFor: Temporal.Instant.from('2099-03-25T08:00:00Z'),
timePrecision: 'date_only_defaulted',
deliveryMode: 'topic',
friendlyTagAssignee: false
@@ -282,7 +282,7 @@ describe('createAdHocNotificationService', () => {
const result = await service.cancelNotification({
notificationId: created.id,
viewerMemberId: 'admin',
asOf: Temporal.Instant.from('2026-03-23T09:00:00Z')
asOf: Temporal.Instant.from('2099-03-23T09:00:00Z')
})
expect(result.status).toBe('cancelled')
@@ -306,7 +306,7 @@ describe('createAdHocNotificationService', () => {
originalRequestText: 'remind tomorrow',
notificationText: 'call landlord',
timezone: 'Asia/Tbilisi',
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
scheduledFor: Temporal.Instant.from('2099-03-25T08:00:00Z'),
timePrecision: 'date_only_defaulted',
deliveryMode: 'topic',
friendlyTagAssignee: false
@@ -315,7 +315,7 @@ describe('createAdHocNotificationService', () => {
const items = await service.listUpcomingNotifications({
householdId: 'household-1',
viewerMemberId: 'viewer',
asOf: Temporal.Instant.from('2026-03-23T09:00:00Z')
asOf: Temporal.Instant.from('2099-03-23T09:00:00Z')
})
expect(items).toHaveLength(1)
@@ -342,7 +342,7 @@ describe('createAdHocNotificationService', () => {
originalRequestText: 'remind tomorrow',
notificationText: 'call landlord',
timezone: 'Asia/Tbilisi',
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
scheduledFor: Temporal.Instant.from('2099-03-25T08:00:00Z'),
timePrecision: 'date_only_defaulted',
deliveryMode: 'topic',
friendlyTagAssignee: false
@@ -355,7 +355,7 @@ describe('createAdHocNotificationService', () => {
timePrecision: 'exact',
deliveryMode: 'dm_selected',
dmRecipientMemberIds: ['alice', 'bob'],
asOf: Temporal.Instant.from('2026-03-23T09:00:00Z')
asOf: Temporal.Instant.from('2099-03-23T09:00:00Z')
})
expect(result.status).toBe('updated')

View File

@@ -8,6 +8,17 @@ import type {
} from '@household/ports'
const BUILT_IN_DISPATCH_KINDS = ['utilities', 'rent_warning', 'rent_due'] as const
const DEFAULT_DUE_DISPATCH_SCAN_LIMIT = 25
const MAX_DUE_DISPATCH_SCAN_LIMIT = 100
function normalizeDueDispatchLimit(limit: number | undefined): number {
const value = limit ?? DEFAULT_DUE_DISPATCH_SCAN_LIMIT
if (!Number.isInteger(value) || value <= 0) {
return DEFAULT_DUE_DISPATCH_SCAN_LIMIT
}
return Math.min(value, MAX_DUE_DISPATCH_SCAN_LIMIT)
}
function builtInDispatchDay(
kind: (typeof BUILT_IN_DISPATCH_KINDS)[number],
@@ -312,7 +323,7 @@ export function createScheduledDispatchService(input: {
return input.repository.listDueScheduledDispatches({
dueBefore: inputValue?.asOf ?? nowInstant(),
provider: input.scheduler.provider,
limit: inputValue?.limit ?? 25
limit: normalizeDueDispatchLimit(inputValue?.limit)
})
},

View File

@@ -22,6 +22,15 @@ require_var() {
fi
}
require_positive_int() {
local name="$1"
local value="${!name:-}"
if [[ ! "$value" =~ ^[0-9]+$ || "$value" -le 0 ]]; then
echo "Invalid positive integer for $name: $value" >&2
exit 1
fi
}
require_var BOT_IMAGE
require_var MINIAPP_IMAGE
require_file "$COMPOSE_FILE"
@@ -38,6 +47,8 @@ export MINIAPP_IMAGE
export ENV_DIR
export SCHEDULER_POLL_INTERVAL_MS="${SCHEDULER_POLL_INTERVAL_MS:-60000}"
export SCHEDULER_DUE_SCAN_LIMIT="${SCHEDULER_DUE_SCAN_LIMIT:-25}"
require_positive_int SCHEDULER_POLL_INTERVAL_MS
require_positive_int SCHEDULER_DUE_SCAN_LIMIT
mkdir -p "$DEPLOY_ROOT"