mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 14:04:04 +00:00
fix: address CI and PR review feedback
Co-authored-by: claw <stanislavkalishin+claw@gmail.com>
This commit is contained in:
24
.github/workflows/cd-vps.yml
vendored
24
.github/workflows/cd-vps.yml
vendored
@@ -29,13 +29,11 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
eligible: ${{ steps.detect.outputs.eligible }}
|
eligible: ${{ steps.detect.outputs.eligible }}
|
||||||
ref: ${{ steps.detect.outputs.ref }}
|
ref: ${{ steps.detect.outputs.ref }}
|
||||||
sha: ${{ steps.detect.outputs.sha }}
|
|
||||||
steps:
|
steps:
|
||||||
- id: detect
|
- id: detect
|
||||||
run: |
|
run: |
|
||||||
eligible=false
|
eligible=false
|
||||||
ref=""
|
ref=""
|
||||||
sha=""
|
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
eligible=true
|
eligible=true
|
||||||
@@ -47,7 +45,6 @@ jobs:
|
|||||||
|
|
||||||
echo "eligible=$eligible" >> "$GITHUB_OUTPUT"
|
echo "eligible=$eligible" >> "$GITHUB_OUTPUT"
|
||||||
echo "ref=$ref" >> "$GITHUB_OUTPUT"
|
echo "ref=$ref" >> "$GITHUB_OUTPUT"
|
||||||
echo "sha=${ref}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
build-bot:
|
build-bot:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -147,11 +144,14 @@ jobs:
|
|||||||
test -n "${{ secrets.VPS_SSH_KEY }}"
|
test -n "${{ secrets.VPS_SSH_KEY }}"
|
||||||
|
|
||||||
- name: Prepare SSH
|
- name: Prepare SSH
|
||||||
|
env:
|
||||||
|
VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
|
||||||
run: |
|
run: |
|
||||||
install -m 700 -d ~/.ssh
|
install -m 700 -d ~/.ssh
|
||||||
printf '%s\n' '${{ secrets.VPS_SSH_KEY }}' > ~/.ssh/id_ed25519
|
printf '%s\n' '${{ secrets.VPS_SSH_KEY }}' > ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.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
|
- name: Upload deploy bundle
|
||||||
run: |
|
run: |
|
||||||
@@ -172,14 +172,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
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
|
- name: Set Telegram webhook
|
||||||
env:
|
env:
|
||||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
@@ -192,3 +184,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
bun run ops:telegram:webhook set
|
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
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export async function createBotRuntimeApp(): Promise<BotRuntimeApp> {
|
|||||||
: null
|
: null
|
||||||
const scheduledDispatchScheduler =
|
const scheduledDispatchScheduler =
|
||||||
runtime.scheduledDispatch &&
|
runtime.scheduledDispatch &&
|
||||||
(runtime.scheduledDispatch.provider === 'self-hosted' || runtime.schedulerSharedSecret)
|
(runtime.scheduledDispatch.provider === 'aws-eventbridge' || runtime.schedulerSharedSecret)
|
||||||
? runtime.scheduledDispatch.provider === 'gcp-cloud-tasks'
|
? runtime.scheduledDispatch.provider === 'gcp-cloud-tasks'
|
||||||
? createGcpScheduledDispatchScheduler({
|
? createGcpScheduledDispatchScheduler({
|
||||||
projectId: runtime.scheduledDispatch.projectId,
|
projectId: runtime.scheduledDispatch.projectId,
|
||||||
|
|||||||
@@ -248,8 +248,14 @@ export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRu
|
|||||||
runtime.schedulerSharedSecret = schedulerSharedSecret
|
runtime.schedulerSharedSecret = schedulerSharedSecret
|
||||||
}
|
}
|
||||||
if (scheduledDispatch !== undefined) {
|
if (scheduledDispatch !== undefined) {
|
||||||
if (scheduledDispatch.provider === 'self-hosted' && schedulerSharedSecret === undefined) {
|
if (
|
||||||
throw new Error('Self-hosted scheduled dispatch requires SCHEDULER_SHARED_SECRET')
|
(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
|
runtime.scheduledDispatch = scheduledDispatch
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ function parsePositiveInteger(name: string, fallback: number): number {
|
|||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runOnce() {
|
async function runOnce(input: {
|
||||||
const baseUrl = requireEnv('BOT_INTERNAL_BASE_URL').replace(/\/$/, '')
|
baseUrl: string
|
||||||
const schedulerSecret = requireEnv('SCHEDULER_SHARED_SECRET')
|
schedulerSecret: string
|
||||||
const dueScanLimit = parsePositiveInteger('SCHEDULER_DUE_SCAN_LIMIT', 25)
|
dueScanLimit: number
|
||||||
|
}) {
|
||||||
const response = await fetch(`${baseUrl}/jobs/dispatch-due?limit=${dueScanLimit}`, {
|
const response = await fetch(`${input.baseUrl}/jobs/dispatch-due?limit=${input.dueScanLimit}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'x-household-scheduler-secret': schedulerSecret
|
'x-household-scheduler-secret': input.schedulerSecret
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -51,6 +51,11 @@ async function runOnce() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const intervalMs = parsePositiveInteger('SCHEDULER_POLL_INTERVAL_MS', 60_000)
|
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
|
let stopping = false
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
@@ -62,7 +67,7 @@ async function main() {
|
|||||||
|
|
||||||
while (!stopping) {
|
while (!stopping) {
|
||||||
try {
|
try {
|
||||||
await runOnce()
|
await runOnce(runConfig)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
|
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
|
||||||
healthcheck:
|
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
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -23,7 +27,9 @@ services:
|
|||||||
scheduler:
|
scheduler:
|
||||||
image: ${BOT_IMAGE:?set BOT_IMAGE}
|
image: ${BOT_IMAGE:?set BOT_IMAGE}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ['bun', 'apps/bot/dist/scheduler-runner.js']
|
command:
|
||||||
|
- bun
|
||||||
|
- apps/bot/dist/scheduler-runner.js
|
||||||
env_file:
|
env_file:
|
||||||
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
|
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
|
||||||
environment:
|
environment:
|
||||||
@@ -36,10 +42,13 @@ services:
|
|||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
image: ${BOT_IMAGE:?set BOT_IMAGE}
|
image: ${BOT_IMAGE:?set BOT_IMAGE}
|
||||||
profiles: ['ops']
|
profiles:
|
||||||
|
- ops
|
||||||
env_file:
|
env_file:
|
||||||
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
|
- ${ENV_DIR:-/opt/household-bot/env}/bot.env
|
||||||
command: ['bun', 'packages/db/dist/migrate.js']
|
command:
|
||||||
|
- bun
|
||||||
|
- packages/db/dist/migrate.js
|
||||||
restart: 'no'
|
restart: 'no'
|
||||||
|
|
||||||
caddy:
|
caddy:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
BOT_API_URL=https://household-bot.whekin.dev
|
VITE_BOT_API_URL=https://household-bot.whekin.dev
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ These can be adjusted later without changing the deployment shape.
|
|||||||
6. Validate builds/tests where practical
|
6. Validate builds/tests where practical
|
||||||
7. Push branch and open PR
|
7. Push branch and open PR
|
||||||
|
|
||||||
|
|
||||||
## Runtime Env Files on VPS
|
## Runtime Env Files on VPS
|
||||||
|
|
||||||
Expected files under `/opt/household-bot/env`:
|
Expected files under `/opt/household-bot/env`:
|
||||||
@@ -134,6 +133,7 @@ Expected files under `/opt/household-bot/env`:
|
|||||||
- `caddy.env`
|
- `caddy.env`
|
||||||
|
|
||||||
Templates live in `deploy/vps/*.env.example`.
|
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
|
## GitHub Actions Inputs / Secrets
|
||||||
|
|
||||||
@@ -145,8 +145,9 @@ Recommended repository variables:
|
|||||||
- `VPS_BOT_URL` (default `https://household-bot.whekin.dev`)
|
- `VPS_BOT_URL` (default `https://household-bot.whekin.dev`)
|
||||||
- `VPS_MINIAPP_URL` (default `https://household.whekin.dev`)
|
- `VPS_MINIAPP_URL` (default `https://household.whekin.dev`)
|
||||||
|
|
||||||
Required repository secret:
|
Required repository secrets:
|
||||||
- `VPS_SSH_KEY`
|
- `VPS_SSH_KEY`
|
||||||
|
- `VPS_KNOWN_HOSTS`
|
||||||
|
|
||||||
Optional for webhook sync and smoke verification:
|
Optional for webhook sync and smoke verification:
|
||||||
- `TELEGRAM_BOT_TOKEN`
|
- `TELEGRAM_BOT_TOKEN`
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ class NotificationRepositoryStub implements AdHocNotificationRepository {
|
|||||||
sentAt: null,
|
sentAt: null,
|
||||||
cancelledAt: null,
|
cancelledAt: null,
|
||||||
cancelledByMemberId: null,
|
cancelledByMemberId: null,
|
||||||
createdAt: Temporal.Instant.from('2026-03-23T09:00:00Z'),
|
createdAt: Temporal.Instant.from('2099-03-23T09:00:00Z'),
|
||||||
updatedAt: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
updatedAt: Temporal.Instant.from('2099-03-23T09:00:00Z')
|
||||||
}
|
}
|
||||||
this.notifications.set(id, record)
|
this.notifications.set(id, record)
|
||||||
return record
|
return record
|
||||||
@@ -191,7 +191,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'Напомни Георгию завтра',
|
originalRequestText: 'Напомни Георгию завтра',
|
||||||
notificationText: 'пошпынять Георгия о том, позвонил ли он',
|
notificationText: 'пошпынять Георгия о том, позвонил ли он',
|
||||||
timezone: 'Asia/Tbilisi',
|
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',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic'
|
deliveryMode: 'topic'
|
||||||
})
|
})
|
||||||
@@ -222,7 +222,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind everyone tomorrow',
|
originalRequestText: 'remind everyone tomorrow',
|
||||||
notificationText: 'pay rent',
|
notificationText: 'pay rent',
|
||||||
timezone: 'Asia/Tbilisi',
|
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',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'dm_all'
|
deliveryMode: 'dm_all'
|
||||||
})
|
})
|
||||||
@@ -246,7 +246,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'check rent',
|
notificationText: 'check rent',
|
||||||
timezone: 'Asia/Tbilisi',
|
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',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: true
|
friendlyTagAssignee: true
|
||||||
@@ -273,7 +273,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'call landlord',
|
notificationText: 'call landlord',
|
||||||
timezone: 'Asia/Tbilisi',
|
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',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: false
|
friendlyTagAssignee: false
|
||||||
@@ -282,7 +282,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
const result = await service.cancelNotification({
|
const result = await service.cancelNotification({
|
||||||
notificationId: created.id,
|
notificationId: created.id,
|
||||||
viewerMemberId: 'admin',
|
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')
|
expect(result.status).toBe('cancelled')
|
||||||
@@ -306,7 +306,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'call landlord',
|
notificationText: 'call landlord',
|
||||||
timezone: 'Asia/Tbilisi',
|
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',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: false
|
friendlyTagAssignee: false
|
||||||
@@ -315,7 +315,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
const items = await service.listUpcomingNotifications({
|
const items = await service.listUpcomingNotifications({
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
viewerMemberId: 'viewer',
|
viewerMemberId: 'viewer',
|
||||||
asOf: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
asOf: Temporal.Instant.from('2099-03-23T09:00:00Z')
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(items).toHaveLength(1)
|
expect(items).toHaveLength(1)
|
||||||
@@ -342,7 +342,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'call landlord',
|
notificationText: 'call landlord',
|
||||||
timezone: 'Asia/Tbilisi',
|
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',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: false
|
friendlyTagAssignee: false
|
||||||
@@ -355,7 +355,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
timePrecision: 'exact',
|
timePrecision: 'exact',
|
||||||
deliveryMode: 'dm_selected',
|
deliveryMode: 'dm_selected',
|
||||||
dmRecipientMemberIds: ['alice', 'bob'],
|
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')
|
expect(result.status).toBe('updated')
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ import type {
|
|||||||
} from '@household/ports'
|
} from '@household/ports'
|
||||||
|
|
||||||
const BUILT_IN_DISPATCH_KINDS = ['utilities', 'rent_warning', 'rent_due'] as const
|
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(
|
function builtInDispatchDay(
|
||||||
kind: (typeof BUILT_IN_DISPATCH_KINDS)[number],
|
kind: (typeof BUILT_IN_DISPATCH_KINDS)[number],
|
||||||
@@ -312,7 +323,7 @@ export function createScheduledDispatchService(input: {
|
|||||||
return input.repository.listDueScheduledDispatches({
|
return input.repository.listDueScheduledDispatches({
|
||||||
dueBefore: inputValue?.asOf ?? nowInstant(),
|
dueBefore: inputValue?.asOf ?? nowInstant(),
|
||||||
provider: input.scheduler.provider,
|
provider: input.scheduler.provider,
|
||||||
limit: inputValue?.limit ?? 25
|
limit: normalizeDueDispatchLimit(inputValue?.limit)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ require_var() {
|
|||||||
fi
|
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 BOT_IMAGE
|
||||||
require_var MINIAPP_IMAGE
|
require_var MINIAPP_IMAGE
|
||||||
require_file "$COMPOSE_FILE"
|
require_file "$COMPOSE_FILE"
|
||||||
@@ -38,6 +47,8 @@ export MINIAPP_IMAGE
|
|||||||
export ENV_DIR
|
export ENV_DIR
|
||||||
export SCHEDULER_POLL_INTERVAL_MS="${SCHEDULER_POLL_INTERVAL_MS:-60000}"
|
export SCHEDULER_POLL_INTERVAL_MS="${SCHEDULER_POLL_INTERVAL_MS:-60000}"
|
||||||
export SCHEDULER_DUE_SCAN_LIMIT="${SCHEDULER_DUE_SCAN_LIMIT:-25}"
|
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"
|
mkdir -p "$DEPLOY_ROOT"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user