feat: add GitHub Action for daily health check cron and backfill script

- Create .github/workflows/health-check.yml to run daily at 2am UTC
- Add scripts/backfill-health-checks.ts to populate health data for existing tools
- Remove health-check from vercel.json crons (now using GitHub Actions)

The GitHub Action workflow follows the same pattern as other sync operations
and calls the /api/sync/health-check endpoint with proper authentication.

The backfill script:
- Fetches all tools from database
- Runs batch health checks with concurrency control (5 tools at a time)
- Shows detailed progress and summary statistics
- Lists broken tools with error details

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-12-04 16:36:55 +10:00
parent f0ecdab824
commit bf8d27a59c
3 changed files with 151 additions and 4 deletions

18
.github/workflows/health-check.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Daily Health Check
on:
schedule:
# Run daily at 2am UTC
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- name: Trigger health check sync
run: |
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/health-check" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-f -s -S -w "\nHTTP Status: %{http_code}\n"

View file

@ -0,0 +1,133 @@
#!/usr/bin/env tsx
/**
* Backfill Health Checks Script
*
* Runs health checks on all existing tools in the database to populate
* initial health status data. This is a one-time script to be run after
* deploying the health check system.
*
* Usage:
* pnpm tsx scripts/backfill-health-checks.ts
*
* Environment Variables Required:
* DATABASE_URL - PostgreSQL connection string
* RAILWAY_EXECUTOR_URL - Railway executor service URL
*/
import { prisma } from '@tpmjs/db';
import { performBatchHealthCheck } from '../apps/web/src/lib/health-check/health-check-service';
async function main() {
console.log('🏥 Starting health check backfill...\n');
try {
// Fetch all tool IDs
console.log('📊 Fetching all tools from database...');
const tools = await prisma.tool.findMany({
select: {
id: true,
exportName: true,
package: {
select: {
npmPackageName: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
});
console.log(`✅ Found ${tools.length} tools to check\n`);
if (tools.length === 0) {
console.log('No tools found in database. Exiting.');
return;
}
// Extract tool IDs
const toolIds = tools.map((t) => t.id);
// Run batch health check with 5 concurrent checks and 1s delay between batches
console.log('🔄 Running batch health checks...');
console.log(' Batch size: 5 concurrent checks');
console.log(' Delay between batches: 1000ms\n');
const startTime = Date.now();
await performBatchHealthCheck(toolIds, 'backfill', 5);
const duration = Date.now() - startTime;
const durationSeconds = (duration / 1000).toFixed(2);
console.log('\n✅ Backfill complete!');
console.log(` Total tools checked: ${tools.length}`);
console.log(` Duration: ${durationSeconds}s`);
console.log(` Average time per tool: ${(duration / tools.length / 1000).toFixed(2)}s\n`);
// Show summary of health status
console.log('📊 Health Status Summary:');
const healthSummary = await prisma.tool.groupBy({
by: ['importHealth', 'executionHealth'],
_count: true,
});
console.log('\nImport Health:');
const importHealthCounts = await prisma.tool.groupBy({
by: ['importHealth'],
_count: true,
});
for (const row of importHealthCounts) {
console.log(` ${row.importHealth || 'UNKNOWN'}: ${row._count}`);
}
console.log('\nExecution Health:');
const executionHealthCounts = await prisma.tool.groupBy({
by: ['executionHealth'],
_count: true,
});
for (const row of executionHealthCounts) {
console.log(` ${row.executionHealth || 'UNKNOWN'}: ${row._count}`);
}
// Show broken tools
const brokenTools = await prisma.tool.findMany({
where: {
OR: [{ importHealth: 'BROKEN' }, { executionHealth: 'BROKEN' }],
},
select: {
exportName: true,
package: {
select: {
npmPackageName: true,
},
},
importHealth: true,
executionHealth: true,
healthCheckError: true,
},
});
if (brokenTools.length > 0) {
console.log(`\n⚠ Broken Tools (${brokenTools.length}):`);
for (const tool of brokenTools) {
console.log(
` - ${tool.package.npmPackageName}/${tool.exportName} (Import: ${tool.importHealth}, Execution: ${tool.executionHealth})`
);
if (tool.healthCheckError) {
console.log(` Error: ${tool.healthCheckError.slice(0, 100)}...`);
}
}
} else {
console.log('\n✅ All tools are healthy!');
}
} catch (error) {
console.error('\n❌ Backfill failed:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();

View file

@ -21,10 +21,6 @@
{
"path": "/api/sync/metrics",
"schedule": "0 * * * *"
},
{
"path": "/api/sync/health-check",
"schedule": "0 2 * * *"
}
]
}