@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.
- package/CHANGELOG.md +13 -0
- package/README.md +161 -520
- package/dist/autoRegisteredGitHubHandlers.d.ts +6 -0
- package/dist/autoRegisteredGitHubHandlers.js +8 -0
- package/dist/compiler.d.ts +25 -0
- package/dist/compiler.js +26 -0
- package/dist/githubEventRouter.d.ts +6 -0
- package/dist/githubEventRouter.js +59 -0
- package/dist/handlers/WebhookHandler.js +66 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +18 -0
- package/dist/types/GitHubEventHandler.d.ts +28 -0
- package/dist/types/GitHubEventHandler.js +2 -0
- package/package.json +14 -4
- package/spec/githubEventRouter.spec.ts +122 -0
- package/src/autoRegisteredGitHubHandlers.ts +7 -0
- package/src/compiler.ts +32 -0
- package/src/githubEventRouter.ts +53 -0
- package/src/handlers/WebhookHandler.ts +48 -1
- package/src/index.ts +5 -0
- package/src/types/GitHubEventHandler.ts +31 -0
|
@@ -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 {};
|
package/dist/compiler.js
ADDED
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/github-app-plugin",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
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.
|
|
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/
|
|
27
|
-
"@flink-app/
|
|
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
|
+
});
|
package/src/compiler.ts
ADDED
|
@@ -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
|
|
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
|
+
}
|