feat: use new `Server` class when using `probot run` binary

This commit is contained in:
Gregor Martynus 2020-11-22 12:30:16 -08:00
parent 966ea5aabe
commit 8a3599db82
8 changed files with 204 additions and 26 deletions

View File

@ -4,7 +4,9 @@ import { Options as PinoOptions } from "@probot/pino";
import { Options } from "../types";
export function readCliOptions(argv: string[]): Options & PinoOptions {
export function readCliOptions(
argv: string[]
): Options & PinoOptions & { args: string[] } {
program
.usage("[options] <apps...>")
.option(

View File

@ -5,6 +5,7 @@ export function readEnvOptions() {
const privateKey = getPrivateKey();
return {
args: [],
privateKey: (privateKey && privateKey.toString()) || undefined,
id: Number(process.env.APP_ID),
port: Number(process.env.PORT) || 3000,

View File

@ -17,7 +17,7 @@
import pino, { LoggerOptions } from "pino";
import { getTransformStream, Options, LogLevel } from "@probot/pino";
type GetLogOptions = { level?: LogLevel } & Options;
export type GetLogOptions = { level?: LogLevel } & Options;
export function getLog(options: GetLogOptions = {}) {
const deprecated = [];

View File

@ -7,6 +7,7 @@ import { Context, WebhookPayloadWithRepository } from "./context";
import { getLog } from "./helpers/get-log";
import { Options } from "./types";
import { Probot } from "./probot";
import { Server } from "./server/server";
import { ProbotOctokit } from "./octokit/probot-octokit";
import { run } from "./run";
@ -27,7 +28,7 @@ export const createProbot = (options: Options) => {
return new Probot(options);
};
export { Logger, Context, Application, ProbotOctokit, run, Probot };
export { Logger, Context, Application, ProbotOctokit, run, Probot, Server };
/** NOTE: exported types might change at any point in time */
export { Options, WebhookPayloadWithRepository };

View File

@ -5,6 +5,7 @@ import { Probot } from "./index";
import { Application } from "./application";
import { ApplicationFunction, ApplicationFunctionOptions } from "./types";
import { getRouter } from "./get-router";
import { resolveAppFunction } from "./helpers/resolve-app-function";
type DeprecatedKey =
| "auth"
@ -37,7 +38,7 @@ function bindMethod(app: Probot | Application, key: keyof Application) {
export function load(
app: Application | Probot,
router: Router | null,
appFn: ApplicationFunction | ApplicationFunction[]
appFn: string | ApplicationFunction | ApplicationFunction[]
) {
const deprecatedApp = DEPRECATED_APP_KEYS.reduce(
(api: Record<string, unknown>, key: DeprecatedKey) => {
@ -64,7 +65,9 @@ export function load(
if (Array.isArray(appFn)) {
appFn.forEach((fn) => load(app, router, fn));
} else {
appFn(
const fn = typeof appFn === "string" ? resolveAppFunction(appFn) : appFn;
fn(
(Object.assign(deprecatedApp, {
app,
getRouter: getRouter.bind(null, router || app.router),

View File

@ -18,7 +18,6 @@ import { getRouter } from "./get-router";
import { getWebhooks } from "./octokit/get-webhooks";
import { load } from "./load";
import { ProbotOctokit } from "./octokit/probot-octokit";
import { resolveAppFunction } from "./helpers/resolve-app-function";
import { run } from "./run";
import { VERSION } from "./version";
import { webhookEventCheck } from "./helpers/webhook-event-check";
@ -188,10 +187,6 @@ export class Probot {
}
public load(appFn: string | ApplicationFunction | ApplicationFunction[]) {
if (typeof appFn === "string") {
appFn = resolveAppFunction(appFn) as ApplicationFunction;
}
const router = express.Router();
// Connect the router from the app to the server
@ -240,6 +235,7 @@ export class Probot {
} else {
this.log.error(error);
}
process.exit(1);
});

View File

@ -1,54 +1,114 @@
require("dotenv").config();
import pkgConf from "pkg-conf";
import program from "commander";
import { ApplicationFunction } from "./types";
import { ApplicationFunction, Options } from "./types";
import { Probot } from "./index";
import { setupAppFactory } from "./apps/setup";
import { logWarningsForObsoleteEnvironmentVariables } from "./helpers/log-warnings-for-obsolete-environment-variables";
import { getLog } from "./helpers/get-log";
import { getLog, GetLogOptions } from "./helpers/get-log";
import { readCliOptions } from "./bin/read-cli-options";
import { readEnvOptions } from "./bin/read-env-options";
import { Server, ServerOptions } from "./server/server";
import { load } from "./load";
import { defaultApp } from "./apps/default";
/**
*
* @param appFnOrArgv set to either a probot application function: `({ app }) => { ... }` or to process.argv
*/
export async function run(appFnOrArgv: ApplicationFunction | string[]) {
const {
// log options
logLevel: level,
logFormat,
logLevelInString,
sentryDsn,
...options
// server options
host,
port,
webhookPath,
webhookProxy,
// probot options
id,
privateKey,
redisConfig,
secret,
baseUrl,
// others
args,
} = Array.isArray(appFnOrArgv)
? readCliOptions(appFnOrArgv)
: readEnvOptions();
const log = getLog({ level, logFormat, logLevelInString, sentryDsn });
const logOptions: GetLogOptions = {
level,
logFormat,
logLevelInString,
sentryDsn,
};
const log = getLog(logOptions);
logWarningsForObsoleteEnvironmentVariables(log);
const probot = new Probot({ log, ...options });
const serverOptions: ServerOptions = {
host,
port,
webhookPath,
webhookProxy,
log: log.child({ name: "server" }),
};
if (!options.id || !options.privateKey) {
const server = new Server(serverOptions);
const router = server.router();
const probotOptions: Options = {
id,
privateKey,
redisConfig,
secret,
baseUrl,
log: log.child({ name: "probot" }),
};
const probot = new Probot(probotOptions);
if (!id || !privateKey) {
if (process.env.NODE_ENV === "production") {
if (!options.id) {
if (!id) {
throw new Error(
"Application ID is missing, and is required to run in production mode. " +
"To resolve, ensure the APP_ID environment variable is set."
);
} else if (!options.privateKey) {
} else if (!privateKey) {
throw new Error(
"Certificate is missing, and is required to run in production mode. " +
"To resolve, ensure either the PRIVATE_KEY or PRIVATE_KEY_PATH environment variable is set and contains a valid certificate"
);
}
}
probot.load(setupAppFactory(options.host, options.port));
load(probot, router, setupAppFactory(host, port));
} else if (Array.isArray(appFnOrArgv)) {
const pkg = await pkgConf("probot");
probot.setup(program.args.concat((pkg.apps as string[]) || []));
} else {
probot.load(appFnOrArgv);
}
probot.start();
load(probot, router, defaultApp);
return probot;
if (Array.isArray(pkg.apps)) {
for (const app of pkg.apps) {
load(probot, router, app);
}
}
const [appFn] = args;
load(probot, router, appFn);
server.app.use(probot.webhooks.middleware);
} else {
load(probot, router, appFnOrArgv);
server.app.use(probot.webhooks.middleware);
}
await server.start();
return server;
}

115
src/server/server.ts Normal file
View File

@ -0,0 +1,115 @@
import { Server as HttpServer } from "http";
import express, { Application, Router } from "express";
import { join } from "path";
import { Logger } from "pino";
import { getLog } from "../helpers/get-log";
import { getLoggingMiddleware } from "./logging-middleware";
import { createWebhookProxy } from "../helpers/webhook-proxy";
import { VERSION } from "../version";
export type ServerOptions = {
log?: Logger;
port?: number;
host?: string;
webhookPath?: string;
webhookProxy?: string;
};
type State = {
httpServer?: HttpServer;
port?: number;
host?: string;
webhookPath?: string;
webhookProxy?: string;
router: Router;
};
export class Server {
public app: Application;
public log: Logger;
public version = VERSION;
private state: State;
constructor(options: ServerOptions = {}) {
this.app = express();
this.log = options.log || getLog().child({ name: "server" });
this.state = {
port: options.port,
host: options.host,
webhookPath: options.webhookPath || "/",
webhookProxy: options.webhookProxy,
router: Router(),
};
this.app.use(getLoggingMiddleware(this.log));
this.app.use(
"/probot/static/",
express.static(join(__dirname, "..", "..", "static"))
);
this.app.set("view engine", "hbs");
this.app.set("views", join(__dirname, "..", "..", "views"));
this.app.get("/ping", (req, res) => res.end("PONG"));
this.app.use(this.state.router);
}
public async start() {
this.log.info(
`Running Probot v${this.version} (Node.js: ${process.version})`
);
const port = this.state.port || 3000;
const { host, webhookPath, webhookProxy } = this.state;
const printableHost = host ?? "localhost";
this.state.httpServer = (await new Promise((resolve, reject) => {
const server = this.app.listen(
port,
...((host ? [host] : []) as any),
() => {
if (webhookProxy) {
createWebhookProxy({
logger: this.log,
path: webhookPath,
port: port,
url: webhookProxy,
});
}
this.log.info(`Listening on http://${printableHost}:${port}`);
resolve(server);
}
);
})) as HttpServer;
this.state.httpServer.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
this.log.error(
`Port ${port} is already in use. You can define the PORT environment variable to use a different port.`
);
} else {
this.log.error(error);
}
process.exit(1);
});
return this.state.httpServer;
}
public async stop() {
if (!this.state.httpServer) return;
const server = this.state.httpServer;
return new Promise((resolve) => server.close(resolve));
}
public router(path?: string) {
if (path) {
const newRouter = Router();
this.state.router.use(path, newRouter);
return newRouter;
}
return this.state.router;
}
}