Swadhin Biswas Feature/ai code reviewer (#73) · 8/2/2026
105a287
106 lines · 3.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

import { RunnerClient } from './client';
import { Executor } from './executor';
import pino from 'pino';

import os from 'node:os';

const logger = pino({
    name: 'runner-main',
    transport: {
        target: 'pino-pretty',
        options: { colorize: true }
    }
});

const CONFIG = {
    apiUrl: process.env.API_URL || process.env.SITE_URL || 'http://localhost:3000',
    token: process.env.RUNNER_TOKEN || '', // Registration token
    name: process.env.RUNNER_NAME || os.hostname(),
    workDir: process.cwd(),
    pollInterval: 5000
};

async function main() {
    logger.info("Starting OpenCodeHub Self-Hosted Runner");
    logger.info({ config: { ...CONFIG, token: '***' } }, "Configuration loaded");

    const client = new RunnerClient(CONFIG);
    const executor = new Executor();

    // Check Docker availability
    const hasDocker = await executor.verifyDocker();
    if (!hasDocker) {
        logger.warn("Docker not detected. Jobs may fail if they rely on container isolation.");
        // In Pro mode, maybe we strictly require Docker? 
        // For now, let's proceed but warn heavily.
    }

    // Initialize (Load creds or Register)
    await client.init();

    // Register if needed
    if (!CONFIG.token && !await client.poll().catch(() => null)) {
        // Hacky check: if poll throws "Not registered" type error, we need token.
        // Actually client.poll() throws if not registered.
    }

    try {
        await client.register();
    } catch (e: any) {
        if (!CONFIG.token) {
            logger.error("Registration failed and no token provided. Set RUNNER_TOKEN env var.");
            process.exit(1);
        }
        // If registration failed but we might have creds? 
        // Client.register checks if creds exist first.
    }

    let isRunning = true;

    // Graceful Shutdown
    const shutdown = () => {
        logger.info("Shutting down runner...");
        isRunning = false;
        process.exit(0);
    };
    process.on('SIGINT', shutdown);
    process.on('SIGTERM', shutdown);

    logger.info("Runner is online and polling for jobs...");

    // Polling Loop
    while (isRunning) {
        try {
            const job = await client.poll();

            if (job) {
                logger.info({ jobId: job.id, jobName: job.name, stepId: job.stepId, stepName: job.stepName }, "Received Job Step");

                const result = await executor.runJob(job.id, job.run);

                await client.completeJob(job.id, {
                    success: result.success,
                    logs: result.logs,
                    exitCode: result.exitCode,
                    stepId: job.stepId
                });
            } else {
                // No job, wait
            }

        } catch (error) {
            logger.error(error, "Error in main loop");
        }

        if (isRunning) {
            await new Promise(r => setTimeout(r, CONFIG.pollInterval));
        }
    }
}

main().catch(err => {
    logger.fatal(err, "Fatal runner error");
    process.exit(1);
});