pi-ghwebhook 0.0.0-stage → 0.1.0
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 +19 -0
- package/LICENSE +21 -0
- package/README.md +167 -2
- package/README_zh.md +168 -0
- package/dist/bus/event-bus.d.ts +14 -0
- package/dist/bus/event-bus.d.ts.map +1 -0
- package/dist/bus/event-bus.js +53 -0
- package/dist/bus/event-bus.js.map +1 -0
- package/dist/bus/guards.d.ts +10 -0
- package/dist/bus/guards.d.ts.map +1 -0
- package/dist/bus/guards.js +88 -0
- package/dist/bus/guards.js.map +1 -0
- package/dist/bus/normalizer.d.ts +14 -0
- package/dist/bus/normalizer.d.ts.map +1 -0
- package/dist/bus/normalizer.js +25 -0
- package/dist/bus/normalizer.js.map +1 -0
- package/dist/config/loader.d.ts +6 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +63 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/template/renderer.d.ts +4 -0
- package/dist/template/renderer.d.ts.map +1 -0
- package/dist/template/renderer.js +121 -0
- package/dist/template/renderer.js.map +1 -0
- package/dist/transport/forwarder.d.ts +18 -0
- package/dist/transport/forwarder.d.ts.map +1 -0
- package/dist/transport/forwarder.js +73 -0
- package/dist/transport/forwarder.js.map +1 -0
- package/dist/transport/loopback.d.ts +20 -0
- package/dist/transport/loopback.d.ts.map +1 -0
- package/dist/transport/loopback.js +96 -0
- package/dist/transport/loopback.js.map +1 -0
- package/dist/types/enums.d.ts +41 -0
- package/dist/types/enums.d.ts.map +1 -0
- package/dist/types/enums.js +44 -0
- package/dist/types/enums.js.map +1 -0
- package/dist/types/payload.d.ts +28 -0
- package/dist/types/payload.d.ts.map +1 -0
- package/dist/types/payload.js +2 -0
- package/dist/types/payload.js.map +1 -0
- package/dist/types/schema.d.ts +59 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +49 -0
- package/dist/types/schema.js.map +1 -0
- package/package.json +64 -4
- package/src/bus/event-bus.ts +72 -0
- package/src/bus/guards.ts +118 -0
- package/src/bus/normalizer.ts +32 -0
- package/src/config/loader.ts +80 -0
- package/src/index.ts +106 -0
- package/src/template/renderer.ts +145 -0
- package/src/transport/forwarder.ts +92 -0
- package/src/transport/loopback.ts +130 -0
- package/src/types/enums.ts +42 -0
- package/src/types/payload.ts +75 -0
- package/src/types/schema.ts +105 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { WebhookPayload } from "../types/payload.js";
|
|
2
|
+
|
|
3
|
+
const VARIABLE_PATTERN = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
|
4
|
+
|
|
5
|
+
function extractRepoVars(
|
|
6
|
+
payload: WebhookPayload,
|
|
7
|
+
vars: Record<string, string>,
|
|
8
|
+
): void {
|
|
9
|
+
if (payload.repository?.full_name) {
|
|
10
|
+
vars.repo = String(payload.repository.full_name);
|
|
11
|
+
vars.repo_url = String(payload.repository.html_url ?? "");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractBranchVars(
|
|
16
|
+
payload: WebhookPayload,
|
|
17
|
+
vars: Record<string, string>,
|
|
18
|
+
fallbackBranch: string,
|
|
19
|
+
): void {
|
|
20
|
+
const branch =
|
|
21
|
+
payload.workflow_run?.head_branch ??
|
|
22
|
+
payload.pull_request?.head?.ref ??
|
|
23
|
+
payload.check_suite?.head_branch ??
|
|
24
|
+
payload.workflow_job?.head_branch ??
|
|
25
|
+
fallbackBranch;
|
|
26
|
+
if (branch) vars.branch = String(branch);
|
|
27
|
+
|
|
28
|
+
const headSha =
|
|
29
|
+
payload.workflow_run?.head_sha ??
|
|
30
|
+
payload.pull_request?.head?.sha ??
|
|
31
|
+
payload.check_suite?.head_sha ??
|
|
32
|
+
payload.after ??
|
|
33
|
+
payload.head_commit?.id;
|
|
34
|
+
if (headSha) vars.head_sha = String(headSha);
|
|
35
|
+
|
|
36
|
+
if (payload.head_commit?.message) {
|
|
37
|
+
vars.commit_message = String(payload.head_commit.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractWorkflowVars(
|
|
42
|
+
payload: WebhookPayload,
|
|
43
|
+
vars: Record<string, string>,
|
|
44
|
+
): void {
|
|
45
|
+
const wf = payload.workflow_run;
|
|
46
|
+
if (!wf) return;
|
|
47
|
+
if (wf.id != null) vars.run_id = String(wf.id);
|
|
48
|
+
if (wf.html_url) vars.run_url = String(wf.html_url);
|
|
49
|
+
if (wf.name) {
|
|
50
|
+
vars.workflow = String(wf.name);
|
|
51
|
+
vars.workflow_name = String(wf.name);
|
|
52
|
+
}
|
|
53
|
+
if (wf.conclusion) vars.conclusion = String(wf.conclusion);
|
|
54
|
+
if (wf.run_number != null) vars.run_number = String(wf.run_number);
|
|
55
|
+
if (wf.run_attempt != null) vars.run_attempt = String(wf.run_attempt);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractPullRequestVars(
|
|
59
|
+
payload: WebhookPayload,
|
|
60
|
+
vars: Record<string, string>,
|
|
61
|
+
): void {
|
|
62
|
+
const pr = payload.pull_request;
|
|
63
|
+
if (!pr) return;
|
|
64
|
+
if (pr.number != null) vars.pr_number = String(pr.number);
|
|
65
|
+
if (pr.title) vars.pr_title = String(pr.title);
|
|
66
|
+
if (pr.html_url) vars.pr_url = String(pr.html_url);
|
|
67
|
+
if (pr.state) vars.pr_state = String(pr.state);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractIssueVars(
|
|
71
|
+
payload: WebhookPayload,
|
|
72
|
+
vars: Record<string, string>,
|
|
73
|
+
): void {
|
|
74
|
+
const iss = payload.issue;
|
|
75
|
+
if (!iss) return;
|
|
76
|
+
if (iss.number != null) vars.issue_number = String(iss.number);
|
|
77
|
+
if (iss.title) vars.issue_title = String(iss.title);
|
|
78
|
+
if (iss.html_url) vars.issue_url = String(iss.html_url);
|
|
79
|
+
if (iss.body != null) vars.issue_body = String(iss.body);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractReviewAndCommentVars(
|
|
83
|
+
payload: WebhookPayload,
|
|
84
|
+
vars: Record<string, string>,
|
|
85
|
+
): void {
|
|
86
|
+
if (payload.review) {
|
|
87
|
+
const rev = payload.review;
|
|
88
|
+
if (rev.state) vars.review_state = String(rev.state);
|
|
89
|
+
if (rev.body != null) vars.review_body = String(rev.body);
|
|
90
|
+
if (rev.html_url) vars.review_url = String(rev.html_url);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (payload.comment) {
|
|
94
|
+
const c = payload.comment;
|
|
95
|
+
if (c.body != null) vars.comment_body = String(c.body);
|
|
96
|
+
if (c.html_url) vars.comment_url = String(c.html_url);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractSenderVars(
|
|
101
|
+
payload: WebhookPayload,
|
|
102
|
+
vars: Record<string, string>,
|
|
103
|
+
): void {
|
|
104
|
+
const senderLogin =
|
|
105
|
+
payload.sender?.login ??
|
|
106
|
+
payload.review?.user?.login ??
|
|
107
|
+
payload.comment?.user?.login;
|
|
108
|
+
if (senderLogin) vars.sender = String(senderLogin);
|
|
109
|
+
|
|
110
|
+
const senderType =
|
|
111
|
+
payload.sender?.type ??
|
|
112
|
+
payload.review?.user?.type ??
|
|
113
|
+
payload.comment?.user?.type;
|
|
114
|
+
if (senderType) vars.sender_type = String(senderType);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function extractVariables(
|
|
118
|
+
payload: WebhookPayload,
|
|
119
|
+
fallbackBranch = "",
|
|
120
|
+
): Record<string, string> {
|
|
121
|
+
const vars: Record<string, string> = {};
|
|
122
|
+
|
|
123
|
+
if (payload.action) {
|
|
124
|
+
vars.action = String(payload.action);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
extractRepoVars(payload, vars);
|
|
128
|
+
extractBranchVars(payload, vars, fallbackBranch);
|
|
129
|
+
extractWorkflowVars(payload, vars);
|
|
130
|
+
extractPullRequestVars(payload, vars);
|
|
131
|
+
extractIssueVars(payload, vars);
|
|
132
|
+
extractReviewAndCommentVars(payload, vars);
|
|
133
|
+
extractSenderVars(payload, vars);
|
|
134
|
+
|
|
135
|
+
return vars;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function renderTemplate(
|
|
139
|
+
template: string,
|
|
140
|
+
vars: Record<string, string>,
|
|
141
|
+
): string {
|
|
142
|
+
return template.replace(VARIABLE_PATTERN, (match, key: string) => {
|
|
143
|
+
return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface ForwarderOptions {
|
|
4
|
+
repo: string;
|
|
5
|
+
url: string;
|
|
6
|
+
events?: string[];
|
|
7
|
+
secret?: string;
|
|
8
|
+
onLog?: (line: string) => void;
|
|
9
|
+
onError?: (err: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class GhForwarder {
|
|
13
|
+
private child: ChildProcess | null = null;
|
|
14
|
+
private readonly options: ForwarderOptions;
|
|
15
|
+
private isStopping = false;
|
|
16
|
+
|
|
17
|
+
constructor(options: ForwarderOptions) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
isRunning(): boolean {
|
|
22
|
+
return this.child !== null && !this.child.killed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
start(): void {
|
|
26
|
+
if (this.child) return;
|
|
27
|
+
|
|
28
|
+
// Default to '*' to listen to ALL events from the repository
|
|
29
|
+
const eventsArg =
|
|
30
|
+
this.options.events && this.options.events.length > 0
|
|
31
|
+
? this.options.events.join(",")
|
|
32
|
+
: "*";
|
|
33
|
+
|
|
34
|
+
const args = [
|
|
35
|
+
"webhook",
|
|
36
|
+
"forward",
|
|
37
|
+
`--repo=${this.options.repo}`,
|
|
38
|
+
`--url=${this.options.url}`,
|
|
39
|
+
`--events=${eventsArg}`,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
if (this.options.secret) {
|
|
43
|
+
args.push(`--secret=${this.options.secret}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.child = spawn("gh", args, {
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.child.stdout?.on("data", (data: Buffer) => {
|
|
52
|
+
const text = data.toString("utf8").trim();
|
|
53
|
+
if (text && this.options.onLog) {
|
|
54
|
+
this.options.onLog(text);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.child.stderr?.on("data", (data: Buffer) => {
|
|
59
|
+
const text = data.toString("utf8").trim();
|
|
60
|
+
if (text && !this.isStopping && this.options.onLog) {
|
|
61
|
+
this.options.onLog(text);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.child.on("error", (err) => {
|
|
66
|
+
if (!this.isStopping && this.options.onError) {
|
|
67
|
+
this.options.onError(err);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.child.on("exit", () => {
|
|
72
|
+
this.child = null;
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (this.options.onError && err instanceof Error) {
|
|
76
|
+
this.options.onError(err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
stop(): void {
|
|
82
|
+
this.isStopping = true;
|
|
83
|
+
if (this.child) {
|
|
84
|
+
try {
|
|
85
|
+
this.child.kill("SIGTERM");
|
|
86
|
+
} catch {
|
|
87
|
+
// Child process may already be dead
|
|
88
|
+
}
|
|
89
|
+
this.child = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createServer,
|
|
3
|
+
type Server,
|
|
4
|
+
type IncomingMessage,
|
|
5
|
+
type ServerResponse,
|
|
6
|
+
} from "node:http";
|
|
7
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
8
|
+
import type { WebhookPayload } from "../types/payload.js";
|
|
9
|
+
|
|
10
|
+
export type WebhookEventHandler = (
|
|
11
|
+
event: string,
|
|
12
|
+
payload: WebhookPayload,
|
|
13
|
+
) => void | Promise<void>;
|
|
14
|
+
|
|
15
|
+
export interface LoopbackOptions {
|
|
16
|
+
secret?: string;
|
|
17
|
+
onEvent: WebhookEventHandler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LoopbackServer {
|
|
21
|
+
private server: Server | null = null;
|
|
22
|
+
private port = 0;
|
|
23
|
+
private readonly secret?: string;
|
|
24
|
+
private readonly onEvent: WebhookEventHandler;
|
|
25
|
+
|
|
26
|
+
constructor(options: LoopbackOptions) {
|
|
27
|
+
this.secret = options.secret;
|
|
28
|
+
this.onEvent = options.onEvent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getPort(): number {
|
|
32
|
+
return this.port;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getUrl(): string {
|
|
36
|
+
return `http://127.0.0.1:${this.port}/webhook`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async start(): Promise<number> {
|
|
40
|
+
this.server = createServer((req, res) => {
|
|
41
|
+
this.handleRequest(req, res);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
this.server!.once("error", reject);
|
|
46
|
+
this.server!.listen(0, "127.0.0.1", () => {
|
|
47
|
+
const addr = this.server!.address();
|
|
48
|
+
if (typeof addr === "object" && addr !== null) {
|
|
49
|
+
this.port = addr.port;
|
|
50
|
+
resolve(this.port);
|
|
51
|
+
} else {
|
|
52
|
+
reject(new Error("Failed to get loopback server port"));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async stop(): Promise<void> {
|
|
59
|
+
if (!this.server) return;
|
|
60
|
+
const s = this.server;
|
|
61
|
+
this.server = null;
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
s.close(() => resolve());
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
|
68
|
+
if (req.method !== "POST") {
|
|
69
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
70
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const event = req.headers["x-github-event"];
|
|
75
|
+
if (typeof event !== "string") {
|
|
76
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
77
|
+
res.end(JSON.stringify({ error: "Missing X-GitHub-Event header" }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const signature = req.headers["x-hub-signature-256"];
|
|
82
|
+
const chunks: Buffer[] = [];
|
|
83
|
+
|
|
84
|
+
req.on("data", (chunk: Buffer) => {
|
|
85
|
+
chunks.push(chunk);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
req.on("end", () => {
|
|
89
|
+
const rawBody = Buffer.concat(chunks);
|
|
90
|
+
|
|
91
|
+
if (this.secret && !this.verifySignature(rawBody, signature)) {
|
|
92
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
93
|
+
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify({ received: true }));
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const payload = JSON.parse(rawBody.toString("utf8")) as WebhookPayload;
|
|
102
|
+
Promise.resolve(this.onEvent(event, payload)).catch((err) => {
|
|
103
|
+
console.error("[pi-ghwebhook] Error handling event:", err);
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error("[pi-ghwebhook] Failed to parse JSON body:", err);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private verifySignature(
|
|
112
|
+
rawBody: Buffer,
|
|
113
|
+
signature: string | string[] | undefined,
|
|
114
|
+
): boolean {
|
|
115
|
+
if (typeof signature !== "string" || !signature.startsWith("sha256=")) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const hmac = createHmac("sha256", this.secret!)
|
|
120
|
+
.update(rawBody)
|
|
121
|
+
.digest("hex");
|
|
122
|
+
const expected = `sha256=${hmac}`;
|
|
123
|
+
|
|
124
|
+
if (signature.length !== expected.length) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard GitHub Webhook Event names as sent in the `X-GitHub-Event` HTTP header.
|
|
3
|
+
*/
|
|
4
|
+
export enum GitHubEvent {
|
|
5
|
+
WorkflowRun = "workflow_run",
|
|
6
|
+
WorkflowJob = "workflow_job",
|
|
7
|
+
PullRequest = "pull_request",
|
|
8
|
+
PullRequestReview = "pull_request_review",
|
|
9
|
+
PullRequestReviewComment = "pull_request_review_comment",
|
|
10
|
+
CheckSuite = "check_suite",
|
|
11
|
+
CheckRun = "check_run",
|
|
12
|
+
IssueComment = "issue_comment",
|
|
13
|
+
Issues = "issues",
|
|
14
|
+
Push = "push",
|
|
15
|
+
Release = "release",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standard conclusions for GitHub Actions workflow runs and check suites,
|
|
20
|
+
* aligned with GitHub OpenAPI specification (@octokit/openapi-webhooks-types).
|
|
21
|
+
*/
|
|
22
|
+
export enum WorkflowConclusion {
|
|
23
|
+
Success = "success",
|
|
24
|
+
Failure = "failure",
|
|
25
|
+
Cancelled = "cancelled",
|
|
26
|
+
TimedOut = "timed_out",
|
|
27
|
+
Neutral = "neutral",
|
|
28
|
+
ActionRequired = "action_required",
|
|
29
|
+
Skipped = "skipped",
|
|
30
|
+
Stale = "stale",
|
|
31
|
+
StartupFailure = "startup_failure",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Review state for pull request reviews.
|
|
36
|
+
*/
|
|
37
|
+
export enum ReviewState {
|
|
38
|
+
Approved = "approved",
|
|
39
|
+
ChangesRequested = "changes_requested",
|
|
40
|
+
Commented = "commented",
|
|
41
|
+
Dismissed = "dismissed",
|
|
42
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { components } from "@octokit/openapi-webhooks-types";
|
|
2
|
+
|
|
3
|
+
export type WebhookSchemas = components["schemas"];
|
|
4
|
+
|
|
5
|
+
// Workflow Run
|
|
6
|
+
export type WorkflowRunCompletedPayload =
|
|
7
|
+
WebhookSchemas["webhook-workflow-run-completed"];
|
|
8
|
+
export type WorkflowRunInProgressPayload =
|
|
9
|
+
WebhookSchemas["webhook-workflow-run-in-progress"];
|
|
10
|
+
export type WorkflowRunRequestedPayload =
|
|
11
|
+
WebhookSchemas["webhook-workflow-run-requested"];
|
|
12
|
+
export type WorkflowRunPayload =
|
|
13
|
+
| WorkflowRunCompletedPayload
|
|
14
|
+
| WorkflowRunInProgressPayload
|
|
15
|
+
| WorkflowRunRequestedPayload;
|
|
16
|
+
|
|
17
|
+
// Pull Request Review
|
|
18
|
+
export type PullRequestReviewSubmittedPayload =
|
|
19
|
+
WebhookSchemas["webhook-pull-request-review-submitted"];
|
|
20
|
+
export type PullRequestReviewEditedPayload =
|
|
21
|
+
WebhookSchemas["webhook-pull-request-review-edited"];
|
|
22
|
+
export type PullRequestReviewDismissedPayload =
|
|
23
|
+
WebhookSchemas["webhook-pull-request-review-dismissed"];
|
|
24
|
+
export type PullRequestReviewPayload =
|
|
25
|
+
| PullRequestReviewSubmittedPayload
|
|
26
|
+
| PullRequestReviewEditedPayload
|
|
27
|
+
| PullRequestReviewDismissedPayload;
|
|
28
|
+
|
|
29
|
+
// Pull Request Review Comment
|
|
30
|
+
export type PullRequestReviewCommentCreatedPayload =
|
|
31
|
+
WebhookSchemas["webhook-pull-request-review-comment-created"];
|
|
32
|
+
export type PullRequestReviewCommentEditedPayload =
|
|
33
|
+
WebhookSchemas["webhook-pull-request-review-comment-edited"];
|
|
34
|
+
export type PullRequestReviewCommentDeletedPayload =
|
|
35
|
+
WebhookSchemas["webhook-pull-request-review-comment-deleted"];
|
|
36
|
+
export type PullRequestReviewCommentPayload =
|
|
37
|
+
| PullRequestReviewCommentCreatedPayload
|
|
38
|
+
| PullRequestReviewCommentEditedPayload
|
|
39
|
+
| PullRequestReviewCommentDeletedPayload;
|
|
40
|
+
|
|
41
|
+
// Check Suite
|
|
42
|
+
export type CheckSuiteCompletedPayload =
|
|
43
|
+
WebhookSchemas["webhook-check-suite-completed"];
|
|
44
|
+
export type CheckSuiteRequestedPayload =
|
|
45
|
+
WebhookSchemas["webhook-check-suite-requested"];
|
|
46
|
+
export type CheckSuiteRerequestedPayload =
|
|
47
|
+
WebhookSchemas["webhook-check-suite-rerequested"];
|
|
48
|
+
export type CheckSuitePayload =
|
|
49
|
+
| CheckSuiteCompletedPayload
|
|
50
|
+
| CheckSuiteRequestedPayload
|
|
51
|
+
| CheckSuiteRerequestedPayload;
|
|
52
|
+
|
|
53
|
+
// Issue Comment
|
|
54
|
+
export type IssueCommentCreatedPayload =
|
|
55
|
+
WebhookSchemas["webhook-issue-comment-created"];
|
|
56
|
+
export type IssueCommentEditedPayload =
|
|
57
|
+
WebhookSchemas["webhook-issue-comment-edited"];
|
|
58
|
+
export type IssueCommentDeletedPayload =
|
|
59
|
+
WebhookSchemas["webhook-issue-comment-deleted"];
|
|
60
|
+
export type IssueCommentPayload =
|
|
61
|
+
| IssueCommentCreatedPayload
|
|
62
|
+
| IssueCommentEditedPayload
|
|
63
|
+
| IssueCommentDeletedPayload;
|
|
64
|
+
|
|
65
|
+
export type SupportedWebhookPayload =
|
|
66
|
+
| WorkflowRunPayload
|
|
67
|
+
| PullRequestReviewPayload
|
|
68
|
+
| PullRequestReviewCommentPayload
|
|
69
|
+
| CheckSuitePayload
|
|
70
|
+
| IssueCommentPayload;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fallback generic webhook payload structure for unknown or untyped events.
|
|
74
|
+
*/
|
|
75
|
+
export type WebhookPayload = Record<string, any>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const FilterSchema = Type.Object(
|
|
4
|
+
{
|
|
5
|
+
branch: Type.Optional(
|
|
6
|
+
Type.String({ description: "Target branch name or '@current'" }),
|
|
7
|
+
),
|
|
8
|
+
conclusion: Type.Optional(
|
|
9
|
+
Type.String({
|
|
10
|
+
description: "Conclusion filter (e.g. 'failure', 'success')",
|
|
11
|
+
}),
|
|
12
|
+
),
|
|
13
|
+
workflow: Type.Optional(
|
|
14
|
+
Type.String({ description: "Workflow name filter (e.g. 'CI')" }),
|
|
15
|
+
),
|
|
16
|
+
state: Type.Optional(
|
|
17
|
+
Type.String({
|
|
18
|
+
description:
|
|
19
|
+
"Review or PR state filter (e.g. 'changes_requested', 'approved')",
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
senders: Type.Optional(
|
|
23
|
+
Type.Array(Type.String(), { description: "Allowed sender logins" }),
|
|
24
|
+
),
|
|
25
|
+
isBot: Type.Optional(
|
|
26
|
+
Type.Boolean({
|
|
27
|
+
description: "Filter for bot (true) or human (false) senders",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
match: Type.Optional(
|
|
31
|
+
Type.Record(
|
|
32
|
+
Type.String(),
|
|
33
|
+
Type.Union([Type.String(), Type.Number(), Type.Boolean()]),
|
|
34
|
+
{
|
|
35
|
+
description:
|
|
36
|
+
"Arbitrary field matching on payload (dot-separated path)",
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
{ additionalProperties: false },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export type FilterConfig = Static<typeof FilterSchema>;
|
|
45
|
+
|
|
46
|
+
export const ActionSchema = Type.Object(
|
|
47
|
+
{
|
|
48
|
+
prompt: Type.String({
|
|
49
|
+
description: "Template string to dispatch to agent",
|
|
50
|
+
}),
|
|
51
|
+
deliverAs: Type.Optional(
|
|
52
|
+
Type.Union([Type.Literal("steer"), Type.Literal("followUp")], {
|
|
53
|
+
description:
|
|
54
|
+
"Delivery urgency: steer interrupts turn, followUp queues next",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
{ additionalProperties: false },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export type ActionConfig = Static<typeof ActionSchema>;
|
|
62
|
+
|
|
63
|
+
export const SubscriptionRuleSchema = Type.Object(
|
|
64
|
+
{
|
|
65
|
+
event: Type.Optional(
|
|
66
|
+
Type.String({
|
|
67
|
+
description:
|
|
68
|
+
"Upstream GitHub Webhook event identifier (e.g. 'workflow_run.completed', 'push', 'issues.opened')",
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
topic: Type.Optional(
|
|
72
|
+
Type.String({
|
|
73
|
+
description:
|
|
74
|
+
"Alias for 'event' (e.g. 'workflow_run.completed' or 'github.workflow_run.completed')",
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
filters: Type.Optional(FilterSchema),
|
|
78
|
+
action: ActionSchema,
|
|
79
|
+
},
|
|
80
|
+
{ additionalProperties: false },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export type SubscriptionRule = Static<typeof SubscriptionRuleSchema>;
|
|
84
|
+
|
|
85
|
+
export const ConfigSchema = Type.Object(
|
|
86
|
+
{
|
|
87
|
+
version: Type.Literal(1, { description: "Schema version" }),
|
|
88
|
+
enabled: Type.Boolean({ default: true }),
|
|
89
|
+
repo: Type.Optional(
|
|
90
|
+
Type.String({
|
|
91
|
+
default: "auto",
|
|
92
|
+
description: "Target GitHub repo (owner/repo or 'auto')",
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
secret: Type.Optional(
|
|
96
|
+
Type.String({ description: "Optional Webhook HMAC secret" }),
|
|
97
|
+
),
|
|
98
|
+
subscriptions: Type.Array(SubscriptionRuleSchema, {
|
|
99
|
+
description: "List of declarative webhook event subscriptions",
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
{ additionalProperties: false },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export type GhWebhookConfig = Static<typeof ConfigSchema>;
|