@flink-app/github-app-plugin 2.0.0-alpha.58 → 2.0.0-alpha.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ import { GitHubEventHandlerFile } from "./types/GitHubEventHandler";
2
+ /**
3
+ * Populated at compile time by the Flink compiler extension.
4
+ * Do not modify manually.
5
+ */
6
+ export declare const autoRegisteredGitHubHandlers: GitHubEventHandlerFile[];
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoRegisteredGitHubHandlers = void 0;
4
+ /**
5
+ * Populated at compile time by the Flink compiler extension.
6
+ * Do not modify manually.
7
+ */
8
+ exports.autoRegisteredGitHubHandlers = [];
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Build-time compiler plugin descriptor for @flink-app/github-app-plugin.
3
+ *
4
+ * Import this from flink.config.js — it MUST NOT import from @flink-app/flink
5
+ * to avoid circular build-time dependencies.
6
+ *
7
+ * Usage in flink.config.js:
8
+ * ```js
9
+ * const { compilerPlugin } = require("@flink-app/github-app-plugin/compiler");
10
+ * module.exports = {
11
+ * compilerPlugins: [compilerPlugin()],
12
+ * };
13
+ * ```
14
+ */
15
+ interface FlinkCompilerPlugin {
16
+ package: string;
17
+ scanDir: string;
18
+ generatedFile: string;
19
+ registrationVar: string;
20
+ detectBy?: (fileContent: string, filePath: string) => boolean;
21
+ }
22
+ export declare function compilerPlugin(opts?: {
23
+ scanDir?: string;
24
+ }): FlinkCompilerPlugin;
25
+ export {};
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * Build-time compiler plugin descriptor for @flink-app/github-app-plugin.
4
+ *
5
+ * Import this from flink.config.js — it MUST NOT import from @flink-app/flink
6
+ * to avoid circular build-time dependencies.
7
+ *
8
+ * Usage in flink.config.js:
9
+ * ```js
10
+ * const { compilerPlugin } = require("@flink-app/github-app-plugin/compiler");
11
+ * module.exports = {
12
+ * compilerPlugins: [compilerPlugin()],
13
+ * };
14
+ * ```
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.compilerPlugin = compilerPlugin;
18
+ function compilerPlugin(opts) {
19
+ return {
20
+ package: "@flink-app/github-app-plugin",
21
+ scanDir: opts?.scanDir ?? "src/github-events",
22
+ generatedFile: "generatedGitHubHandlers",
23
+ registrationVar: "autoRegisteredGitHubHandlers",
24
+ detectBy: (fileContent) => fileContent.includes("GitHubEventHandler"),
25
+ };
26
+ }
@@ -0,0 +1,6 @@
1
+ import { GitHubEventRouteProps } from "./types/GitHubEventHandler";
2
+ /**
3
+ * Returns true if the webhook event matches the given route criteria.
4
+ * An empty/undefined Route always matches (catch-all).
5
+ */
6
+ export declare function matchesRoute(event: string, action: string | undefined, payload: Record<string, any>, installationId: number, route: GitHubEventRouteProps): boolean;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesRoute = matchesRoute;
4
+ /**
5
+ * Returns true if the webhook event matches the given route criteria.
6
+ * An empty/undefined Route always matches (catch-all).
7
+ */
8
+ function matchesRoute(event, action, payload, installationId, route) {
9
+ if (route.event !== undefined) {
10
+ if (Array.isArray(route.event)) {
11
+ if (!route.event.includes(event))
12
+ return false;
13
+ }
14
+ else {
15
+ if (event !== route.event)
16
+ return false;
17
+ }
18
+ }
19
+ if (route.action !== undefined) {
20
+ if (action === undefined)
21
+ return false;
22
+ if (Array.isArray(route.action)) {
23
+ if (!route.action.includes(action))
24
+ return false;
25
+ }
26
+ else {
27
+ if (action !== route.action)
28
+ return false;
29
+ }
30
+ }
31
+ if (route.repository !== undefined) {
32
+ const repoFullName = payload.repository?.full_name;
33
+ if (!repoFullName)
34
+ return false;
35
+ if (typeof route.repository === "function") {
36
+ if (!route.repository(payload))
37
+ return false;
38
+ }
39
+ else if (route.repository instanceof RegExp) {
40
+ if (!route.repository.test(repoFullName))
41
+ return false;
42
+ }
43
+ else {
44
+ if (repoFullName !== route.repository)
45
+ return false;
46
+ }
47
+ }
48
+ if (route.installationId !== undefined) {
49
+ if (Array.isArray(route.installationId)) {
50
+ if (!route.installationId.includes(installationId))
51
+ return false;
52
+ }
53
+ else {
54
+ if (installationId !== route.installationId)
55
+ return false;
56
+ }
57
+ }
58
+ return true;
59
+ }
@@ -11,9 +11,35 @@
11
11
  *
12
12
  * Route: POST /github-app/webhook
13
13
  */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
14
37
  Object.defineProperty(exports, "__esModule", { value: true });
15
38
  exports.Route = void 0;
39
+ const crypto = __importStar(require("crypto"));
16
40
  const flink_1 = require("@flink-app/flink");
41
+ const autoRegisteredGitHubHandlers_1 = require("../autoRegisteredGitHubHandlers");
42
+ const githubEventRouter_1 = require("../githubEventRouter");
17
43
  /**
18
44
  * Route configuration
19
45
  * Registered programmatically by the plugin if registerRoutes is enabled
@@ -136,6 +162,46 @@ const WebhookHandler = async ({ ctx, req }) => {
136
162
  }
137
163
  }
138
164
  }
165
+ // Run auto-registered event handlers
166
+ if (installationId && autoRegisteredGitHubHandlers_1.autoRegisteredGitHubHandlers.length > 0) {
167
+ try {
168
+ const github = await ctx.plugins.githubApp.getClient(installationId);
169
+ for (const h of autoRegisteredGitHubHandlers_1.autoRegisteredGitHubHandlers) {
170
+ if (!(0, githubEventRouter_1.matchesRoute)(event, action, payload, installationId, h.Route ?? {}))
171
+ continue;
172
+ try {
173
+ await flink_1.requestContext.run({ reqId: crypto.randomUUID(), timestamp: Date.now() }, () => h.default({
174
+ ctx,
175
+ event,
176
+ action,
177
+ payload,
178
+ installationId,
179
+ deliveryId,
180
+ github,
181
+ }));
182
+ }
183
+ catch (error) {
184
+ flink_1.log.error("Error in GitHub event handler", {
185
+ event,
186
+ action,
187
+ installationId,
188
+ deliveryId,
189
+ file: h.__file,
190
+ error: error.message,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ catch (error) {
196
+ flink_1.log.error("Failed to create GitHub API client for event handlers", {
197
+ event,
198
+ action,
199
+ installationId,
200
+ deliveryId,
201
+ error: error.message,
202
+ });
203
+ }
204
+ }
139
205
  // Return 200 OK to GitHub to acknowledge receipt
140
206
  return {
141
207
  status: 200,
package/dist/index.d.ts CHANGED
@@ -17,4 +17,7 @@ export type { default as WebhookPayload } from "./schemas/WebhookPayload";
17
17
  export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from "./services/GitHubAPIClient";
18
18
  export { GitHubAuthService } from "./services/GitHubAuthService";
19
19
  export { InstallationService, type CompleteInstallationResult } from "./services/InstallationService";
20
+ export * from "./types/GitHubEventHandler";
21
+ export * from "./autoRegisteredGitHubHandlers";
22
+ export * from "./githubEventRouter";
20
23
  export { GitHubAppErrorCodes } from "./utils/error-utils";
package/dist/index.js CHANGED
@@ -8,6 +8,20 @@
8
8
  * - Webhook handling with signature validation
9
9
  * - API client wrapper
10
10
  */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
23
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
24
+ };
11
25
  Object.defineProperty(exports, "__esModule", { value: true });
12
26
  exports.GitHubAppErrorCodes = exports.InstallationService = exports.GitHubAuthService = exports.GitHubAPIClient = exports.githubAppPlugin = void 0;
13
27
  // Plugin factory
@@ -20,6 +34,10 @@ var GitHubAuthService_1 = require("./services/GitHubAuthService");
20
34
  Object.defineProperty(exports, "GitHubAuthService", { enumerable: true, get: function () { return GitHubAuthService_1.GitHubAuthService; } });
21
35
  var InstallationService_1 = require("./services/InstallationService");
22
36
  Object.defineProperty(exports, "InstallationService", { enumerable: true, get: function () { return InstallationService_1.InstallationService; } });
37
+ // Event handler types and auto-registration
38
+ __exportStar(require("./types/GitHubEventHandler"), exports);
39
+ __exportStar(require("./autoRegisteredGitHubHandlers"), exports);
40
+ __exportStar(require("./githubEventRouter"), exports);
23
41
  // Error utilities
24
42
  var error_utils_1 = require("./utils/error-utils");
25
43
  Object.defineProperty(exports, "GitHubAppErrorCodes", { enumerable: true, get: function () { return error_utils_1.GitHubAppErrorCodes; } });
@@ -0,0 +1,28 @@
1
+ import { GitHubAPIClient } from "../services/GitHubAPIClient";
2
+ /** Route matching criteria for a GitHubEventHandler */
3
+ export interface GitHubEventRouteProps {
4
+ /** Match by event type (e.g. "push", "pull_request") */
5
+ event?: string | string[];
6
+ /** Match by action (e.g. "opened", "closed") */
7
+ action?: string | string[];
8
+ /** Match by repository full name (e.g. "owner/repo") */
9
+ repository?: string | RegExp | ((payload: Record<string, any>) => boolean);
10
+ /** Match by installation ID */
11
+ installationId?: number | number[];
12
+ }
13
+ /** Handler function type */
14
+ export type GitHubEventHandler<TCtx = any> = (args: {
15
+ ctx: TCtx;
16
+ event: string;
17
+ action?: string;
18
+ payload: Record<string, any>;
19
+ installationId: number;
20
+ deliveryId: string;
21
+ github: GitHubAPIClient;
22
+ }) => Promise<void>;
23
+ /** Handler file shape (populated by compiler) */
24
+ export interface GitHubEventHandlerFile<TCtx = any> {
25
+ Route?: GitHubEventRouteProps;
26
+ default: GitHubEventHandler<TCtx>;
27
+ __file?: string;
28
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "@flink-app/github-app-plugin",
3
- "version": "2.0.0-alpha.58",
3
+ "version": "2.0.0-alpha.60",
4
4
  "description": "Flink plugin for GitHub App integration with installation management and webhook handling",
5
5
  "author": "joel@frost.se",
6
6
  "license": "MIT",
7
7
  "types": "dist/index.d.ts",
8
8
  "main": "dist/index.js",
9
+ "exports": {
10
+ ".": {
11
+ "default": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./compiler": {
15
+ "default": "./dist/compiler.js",
16
+ "types": "./dist/compiler.d.ts"
17
+ }
18
+ },
9
19
  "publishConfig": {
10
20
  "access": "public"
11
21
  },
12
22
  "peerDependencies": {
13
23
  "mongodb": "^6.15.0",
14
- "@flink-app/flink": ">=2.0.0-alpha.58"
24
+ "@flink-app/flink": ">=2.0.0-alpha.60"
15
25
  },
16
26
  "dependencies": {
17
27
  "jsonwebtoken": "^9.0.2"
@@ -23,8 +33,8 @@
23
33
  "mongodb-memory-server": "^10.2.3",
24
34
  "ts-node": "^10.9.2",
25
35
  "tsc-watch": "^4.2.9",
26
- "@flink-app/test-utils": "2.0.0-alpha.58",
27
- "@flink-app/flink": "2.0.0-alpha.58"
36
+ "@flink-app/flink": "2.0.0-alpha.60",
37
+ "@flink-app/test-utils": "2.0.0-alpha.60"
28
38
  },
29
39
  "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
30
40
  "scripts": {
@@ -0,0 +1,122 @@
1
+ import { matchesRoute } from "../src/githubEventRouter";
2
+ import { GitHubEventRouteProps } from "../src/types/GitHubEventHandler";
3
+
4
+ describe("githubEventRouter", () => {
5
+ describe("matchesRoute", () => {
6
+ const basePayload = {
7
+ repository: { full_name: "owner/repo" },
8
+ sender: { login: "user1" },
9
+ };
10
+
11
+ it("should match everything with empty route", () => {
12
+ expect(matchesRoute("push", undefined, basePayload, 123, {})).toBe(true);
13
+ });
14
+
15
+ // Event matching
16
+ it("should match by event string", () => {
17
+ expect(matchesRoute("push", undefined, basePayload, 123, { event: "push" })).toBe(true);
18
+ });
19
+
20
+ it("should not match wrong event string", () => {
21
+ expect(matchesRoute("push", undefined, basePayload, 123, { event: "pull_request" })).toBe(false);
22
+ });
23
+
24
+ it("should match by event array", () => {
25
+ expect(matchesRoute("push", undefined, basePayload, 123, { event: ["push", "pull_request"] })).toBe(true);
26
+ });
27
+
28
+ it("should not match event not in array", () => {
29
+ expect(matchesRoute("issues", undefined, basePayload, 123, { event: ["push", "pull_request"] })).toBe(false);
30
+ });
31
+
32
+ // Action matching
33
+ it("should match by action string", () => {
34
+ expect(matchesRoute("pull_request", "opened", basePayload, 123, { action: "opened" })).toBe(true);
35
+ });
36
+
37
+ it("should not match wrong action string", () => {
38
+ expect(matchesRoute("pull_request", "closed", basePayload, 123, { action: "opened" })).toBe(false);
39
+ });
40
+
41
+ it("should not match when action is undefined but route requires action", () => {
42
+ expect(matchesRoute("push", undefined, basePayload, 123, { action: "opened" })).toBe(false);
43
+ });
44
+
45
+ it("should match by action array", () => {
46
+ expect(matchesRoute("pull_request", "closed", basePayload, 123, { action: ["opened", "closed"] })).toBe(true);
47
+ });
48
+
49
+ it("should not match action not in array", () => {
50
+ expect(matchesRoute("pull_request", "reopened", basePayload, 123, { action: ["opened", "closed"] })).toBe(false);
51
+ });
52
+
53
+ // Repository matching
54
+ it("should match by repository string", () => {
55
+ expect(matchesRoute("push", undefined, basePayload, 123, { repository: "owner/repo" })).toBe(true);
56
+ });
57
+
58
+ it("should not match wrong repository string", () => {
59
+ expect(matchesRoute("push", undefined, basePayload, 123, { repository: "other/repo" })).toBe(false);
60
+ });
61
+
62
+ it("should match by repository regex", () => {
63
+ expect(matchesRoute("push", undefined, basePayload, 123, { repository: /^owner\// })).toBe(true);
64
+ });
65
+
66
+ it("should not match repository regex that doesn't match", () => {
67
+ expect(matchesRoute("push", undefined, basePayload, 123, { repository: /^other\// })).toBe(false);
68
+ });
69
+
70
+ it("should match by repository function", () => {
71
+ const fn = (payload: Record<string, any>) => payload.repository.full_name.startsWith("owner");
72
+ expect(matchesRoute("push", undefined, basePayload, 123, { repository: fn })).toBe(true);
73
+ });
74
+
75
+ it("should not match repository function returning false", () => {
76
+ const fn = () => false;
77
+ expect(matchesRoute("push", undefined, basePayload, 123, { repository: fn })).toBe(false);
78
+ });
79
+
80
+ it("should not match when payload has no repository", () => {
81
+ expect(matchesRoute("push", undefined, {}, 123, { repository: "owner/repo" })).toBe(false);
82
+ });
83
+
84
+ // InstallationId matching
85
+ it("should match by installationId number", () => {
86
+ expect(matchesRoute("push", undefined, basePayload, 123, { installationId: 123 })).toBe(true);
87
+ });
88
+
89
+ it("should not match wrong installationId number", () => {
90
+ expect(matchesRoute("push", undefined, basePayload, 123, { installationId: 456 })).toBe(false);
91
+ });
92
+
93
+ it("should match by installationId array", () => {
94
+ expect(matchesRoute("push", undefined, basePayload, 123, { installationId: [123, 456] })).toBe(true);
95
+ });
96
+
97
+ it("should not match installationId not in array", () => {
98
+ expect(matchesRoute("push", undefined, basePayload, 123, { installationId: [456, 789] })).toBe(false);
99
+ });
100
+
101
+ // Combined criteria (AND logic)
102
+ it("should match when all criteria match", () => {
103
+ const route: GitHubEventRouteProps = {
104
+ event: "pull_request",
105
+ action: "opened",
106
+ repository: "owner/repo",
107
+ installationId: 123,
108
+ };
109
+ expect(matchesRoute("pull_request", "opened", basePayload, 123, route)).toBe(true);
110
+ });
111
+
112
+ it("should not match when one criterion fails", () => {
113
+ const route: GitHubEventRouteProps = {
114
+ event: "pull_request",
115
+ action: "opened",
116
+ repository: "owner/repo",
117
+ installationId: 456, // wrong
118
+ };
119
+ expect(matchesRoute("pull_request", "opened", basePayload, 123, route)).toBe(false);
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,7 @@
1
+ import { GitHubEventHandlerFile } from "./types/GitHubEventHandler";
2
+
3
+ /**
4
+ * Populated at compile time by the Flink compiler extension.
5
+ * Do not modify manually.
6
+ */
7
+ export const autoRegisteredGitHubHandlers: GitHubEventHandlerFile[] = [];
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build-time compiler plugin descriptor for @flink-app/github-app-plugin.
3
+ *
4
+ * Import this from flink.config.js — it MUST NOT import from @flink-app/flink
5
+ * to avoid circular build-time dependencies.
6
+ *
7
+ * Usage in flink.config.js:
8
+ * ```js
9
+ * const { compilerPlugin } = require("@flink-app/github-app-plugin/compiler");
10
+ * module.exports = {
11
+ * compilerPlugins: [compilerPlugin()],
12
+ * };
13
+ * ```
14
+ */
15
+
16
+ interface FlinkCompilerPlugin {
17
+ package: string;
18
+ scanDir: string;
19
+ generatedFile: string;
20
+ registrationVar: string;
21
+ detectBy?: (fileContent: string, filePath: string) => boolean;
22
+ }
23
+
24
+ export function compilerPlugin(opts?: { scanDir?: string }): FlinkCompilerPlugin {
25
+ return {
26
+ package: "@flink-app/github-app-plugin",
27
+ scanDir: opts?.scanDir ?? "src/github-events",
28
+ generatedFile: "generatedGitHubHandlers",
29
+ registrationVar: "autoRegisteredGitHubHandlers",
30
+ detectBy: (fileContent) => fileContent.includes("GitHubEventHandler"),
31
+ };
32
+ }
@@ -0,0 +1,53 @@
1
+ import { GitHubEventRouteProps } from "./types/GitHubEventHandler";
2
+
3
+ /**
4
+ * Returns true if the webhook event matches the given route criteria.
5
+ * An empty/undefined Route always matches (catch-all).
6
+ */
7
+ export function matchesRoute(
8
+ event: string,
9
+ action: string | undefined,
10
+ payload: Record<string, any>,
11
+ installationId: number,
12
+ route: GitHubEventRouteProps
13
+ ): boolean {
14
+ if (route.event !== undefined) {
15
+ if (Array.isArray(route.event)) {
16
+ if (!route.event.includes(event)) return false;
17
+ } else {
18
+ if (event !== route.event) return false;
19
+ }
20
+ }
21
+
22
+ if (route.action !== undefined) {
23
+ if (action === undefined) return false;
24
+ if (Array.isArray(route.action)) {
25
+ if (!route.action.includes(action)) return false;
26
+ } else {
27
+ if (action !== route.action) return false;
28
+ }
29
+ }
30
+
31
+ if (route.repository !== undefined) {
32
+ const repoFullName = payload.repository?.full_name;
33
+ if (!repoFullName) return false;
34
+
35
+ if (typeof route.repository === "function") {
36
+ if (!route.repository(payload)) return false;
37
+ } else if (route.repository instanceof RegExp) {
38
+ if (!route.repository.test(repoFullName)) return false;
39
+ } else {
40
+ if (repoFullName !== route.repository) return false;
41
+ }
42
+ }
43
+
44
+ if (route.installationId !== undefined) {
45
+ if (Array.isArray(route.installationId)) {
46
+ if (!route.installationId.includes(installationId)) return false;
47
+ } else {
48
+ if (installationId !== route.installationId) return false;
49
+ }
50
+ }
51
+
52
+ return true;
53
+ }
@@ -11,8 +11,11 @@
11
11
  * Route: POST /github-app/webhook
12
12
  */
13
13
 
14
- import { HttpMethod, RouteProps, log } from "@flink-app/flink";
14
+ import * as crypto from "crypto";
15
+ import { HttpMethod, RouteProps, log, requestContext } from "@flink-app/flink";
15
16
  import { GitHubAppInternalContext } from "../GitHubAppInternalContext";
17
+ import { autoRegisteredGitHubHandlers } from "../autoRegisteredGitHubHandlers";
18
+ import { matchesRoute } from "../githubEventRouter";
16
19
 
17
20
  /**
18
21
  * Route configuration
@@ -149,6 +152,50 @@ const WebhookHandler = async ({ ctx, req }: { ctx: GitHubAppInternalContext; req
149
152
  }
150
153
  }
151
154
 
155
+ // Run auto-registered event handlers
156
+ if (installationId && autoRegisteredGitHubHandlers.length > 0) {
157
+ try {
158
+ const github = await ctx.plugins.githubApp.getClient(installationId);
159
+
160
+ for (const h of autoRegisteredGitHubHandlers) {
161
+ if (!matchesRoute(event, action, payload, installationId, h.Route ?? {})) continue;
162
+
163
+ try {
164
+ await requestContext.run(
165
+ { reqId: crypto.randomUUID(), timestamp: Date.now() },
166
+ () =>
167
+ h.default({
168
+ ctx,
169
+ event,
170
+ action,
171
+ payload,
172
+ installationId,
173
+ deliveryId,
174
+ github,
175
+ })
176
+ );
177
+ } catch (error: any) {
178
+ log.error("Error in GitHub event handler", {
179
+ event,
180
+ action,
181
+ installationId,
182
+ deliveryId,
183
+ file: h.__file,
184
+ error: error.message,
185
+ });
186
+ }
187
+ }
188
+ } catch (error: any) {
189
+ log.error("Failed to create GitHub API client for event handlers", {
190
+ event,
191
+ action,
192
+ installationId,
193
+ deliveryId,
194
+ error: error.message,
195
+ });
196
+ }
197
+ }
198
+
152
199
  // Return 200 OK to GitHub to acknowledge receipt
153
200
  return {
154
201
  status: 200,
package/src/index.ts CHANGED
@@ -29,5 +29,10 @@ export { GitHubAPIClient, type Repository, type Content, type Issue, type Create
29
29
  export { GitHubAuthService } from "./services/GitHubAuthService";
30
30
  export { InstallationService, type CompleteInstallationResult } from "./services/InstallationService";
31
31
 
32
+ // Event handler types and auto-registration
33
+ export * from "./types/GitHubEventHandler";
34
+ export * from "./autoRegisteredGitHubHandlers";
35
+ export * from "./githubEventRouter";
36
+
32
37
  // Error utilities
33
38
  export { GitHubAppErrorCodes } from "./utils/error-utils";
@@ -0,0 +1,31 @@
1
+ import { GitHubAPIClient } from "../services/GitHubAPIClient";
2
+
3
+ /** Route matching criteria for a GitHubEventHandler */
4
+ export interface GitHubEventRouteProps {
5
+ /** Match by event type (e.g. "push", "pull_request") */
6
+ event?: string | string[];
7
+ /** Match by action (e.g. "opened", "closed") */
8
+ action?: string | string[];
9
+ /** Match by repository full name (e.g. "owner/repo") */
10
+ repository?: string | RegExp | ((payload: Record<string, any>) => boolean);
11
+ /** Match by installation ID */
12
+ installationId?: number | number[];
13
+ }
14
+
15
+ /** Handler function type */
16
+ export type GitHubEventHandler<TCtx = any> = (args: {
17
+ ctx: TCtx;
18
+ event: string;
19
+ action?: string;
20
+ payload: Record<string, any>;
21
+ installationId: number;
22
+ deliveryId: string;
23
+ github: GitHubAPIClient;
24
+ }) => Promise<void>;
25
+
26
+ /** Handler file shape (populated by compiler) */
27
+ export interface GitHubEventHandlerFile<TCtx = any> {
28
+ Route?: GitHubEventRouteProps;
29
+ default: GitHubEventHandler<TCtx>;
30
+ __file?: string;
31
+ }