probot
Version:
A framework for building GitHub Apps to automate and improve your workflow
179 lines • 6.99 kB
JavaScript
import { Server as HttpServer } from "node:http";
import { VERSION } from "../version.js";
import { createDeferredPromise, } from "../helpers/create-deferred-promise.js";
import { createWebhookProxy } from "../helpers/webhook-proxy.js";
import { getPrintableHost } from "../helpers/get-printable-host.js";
import { getRuntimeName } from "../helpers/get-runtime-name.js";
import { getRuntimeVersion } from "../helpers/get-runtime-version.js";
import { httpLogger } from "./handlers/http-logger.js";
import { notFoundHandler } from "./handlers/not-found.js";
import { pingHandler } from "./handlers/ping.js";
import { staticFilesHandler } from "./handlers/static-files.js";
import { getLog } from "../helpers/get-log.js";
// the default path as defined in @octokit/webhooks
export const defaultWebhookPath = "/api/github/webhooks";
export const defaultWebhookSecret = "development";
export class Server {
#state;
constructor(options = {}) {
this.#state = {
initializeRan: false,
initializationPromise: createDeferredPromise(),
probot: null,
log: options.log,
httpServer: new HttpServer(),
cwd: options.cwd || process.cwd(),
port: options.port || 3000,
host: options.host || "localhost",
webhookPath: options.webhookPath || defaultWebhookPath,
webhookProxy: options.webhookProxy,
eventSource: undefined,
request: options.request,
ProbotBase: options.Probot,
addedHandlers: [],
handlers: [],
enablePing: options.enablePing ?? true,
enableNotFound: options.enableNotFound ?? true,
enableStaticFiles: options.enableStaticFiles ?? true,
};
this.#state.httpServer.on("request", async (req, res) => {
this.#state.httpLogger(req, res);
try {
for (const handler of this.#state.handlers) {
if (await handler(req, res)) {
return true;
}
}
}
catch (e) {
this.#state.log.error(e);
res.writeHead(500).end();
return true;
}
return false;
});
}
async #initialize() {
if (this.#state.initializeRan === true) {
return this.#state.initializationPromise.promise;
}
this.#state.initializeRan = true;
try {
this.#state.log = this.#state.log || (await getLog());
this.#state.log.child({ name: "server" });
this.#state.probot = new this.#state.ProbotBase({
request: this.#state.request,
log: this.#state.log,
server: this,
webhookPath: this.#state.webhookPath,
});
await this.#state.probot.ready();
this.#state.httpLogger = httpLogger(this.#state.log, this.#state.loggingOptions);
this.#state.initializationPromise.resolve();
}
catch (error) {
this.#state.initializationPromise.reject(error);
(this.#state.log || console).error({ err: error }, "Failed to initialize Server");
throw error;
}
return this.#state.initializationPromise.promise;
}
addHandler(handler) {
this.#state.addedHandlers.push(handler);
}
async loadHandlerFactory(appFn) {
await this.#initialize();
const handler = await appFn(this.#state.probot, {
cwd: this.#state.cwd,
addHandler: (handler) => {
this.addHandler(handler);
},
});
this.addHandler(handler);
}
async load(appFn) {
await this.#initialize();
await appFn(this.#state.probot, {
cwd: this.#state.cwd,
addHandler: (handler) => {
this.addHandler(handler);
},
});
}
async start() {
await this.#initialize();
this.#state.handlers = [];
if (this.#state.enableStaticFiles === true) {
this.#state.handlers.unshift(staticFilesHandler);
}
if (this.#state.enablePing === true) {
this.#state.handlers.unshift(pingHandler);
}
this.#state.handlers.unshift(await this.#state.probot.getNodeMiddleware());
this.#state.handlers.push(...this.#state.addedHandlers);
if (this.#state.enableNotFound === true) {
this.#state.handlers.push(notFoundHandler);
}
const runtimeName = getRuntimeName(globalThis);
const runtimeVersion = getRuntimeVersion(globalThis);
this.#state.log.info(`Running Probot v${VERSION} (${runtimeName}: ${runtimeVersion})`);
const printableHost = getPrintableHost(this.#state.host);
this.#state.httpServer = await new Promise((resolve, reject) => {
const server = this.#state.httpServer.listen({ port: this.#state.port, host: this.#state.host }, () => {
const { port, address, family } = server.address();
this.#state.port = port;
this.#state.host = address;
if (family === "IPv6") {
this.#state.host = `[${address}]`;
}
this.#state.log.info(`Listening on http://${printableHost}:${port}`);
resolve(server);
});
server.on("error", (error) => {
if (error.code === "EADDRINUSE") {
error = Object.assign(error, {
message: `Port ${this.#state.port} is already in use. You can define the PORT environment variable to use a different port.`,
});
}
this.#state.log.error(error);
reject(error);
});
});
if (this.#state.webhookProxy) {
this.#state.eventSource = await createWebhookProxy({
host: this.#state.host,
port: this.#state.port,
path: this.#state.webhookPath,
logger: this.#state.log,
url: this.#state.webhookProxy,
});
}
return this.#state.httpServer;
}
async stop() {
if (this.#state.eventSource) {
this.#state.eventSource.close();
this.#state.eventSource = undefined;
}
if (this.#state.httpServer.listening === false) {
return;
}
const server = this.#state.httpServer;
return new Promise((resolve, reject) => server.close((err) => {
err ? reject(err) : resolve();
}));
}
get port() {
return this.#state.port;
}
get host() {
return this.#state.host;
}
static get version() {
return VERSION;
}
get version() {
return VERSION;
}
}
//# sourceMappingURL=server.js.map