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,28 @@
|
|
|
1
|
+
import type { components } from "@octokit/openapi-webhooks-types";
|
|
2
|
+
export type WebhookSchemas = components["schemas"];
|
|
3
|
+
export type WorkflowRunCompletedPayload = WebhookSchemas["webhook-workflow-run-completed"];
|
|
4
|
+
export type WorkflowRunInProgressPayload = WebhookSchemas["webhook-workflow-run-in-progress"];
|
|
5
|
+
export type WorkflowRunRequestedPayload = WebhookSchemas["webhook-workflow-run-requested"];
|
|
6
|
+
export type WorkflowRunPayload = WorkflowRunCompletedPayload | WorkflowRunInProgressPayload | WorkflowRunRequestedPayload;
|
|
7
|
+
export type PullRequestReviewSubmittedPayload = WebhookSchemas["webhook-pull-request-review-submitted"];
|
|
8
|
+
export type PullRequestReviewEditedPayload = WebhookSchemas["webhook-pull-request-review-edited"];
|
|
9
|
+
export type PullRequestReviewDismissedPayload = WebhookSchemas["webhook-pull-request-review-dismissed"];
|
|
10
|
+
export type PullRequestReviewPayload = PullRequestReviewSubmittedPayload | PullRequestReviewEditedPayload | PullRequestReviewDismissedPayload;
|
|
11
|
+
export type PullRequestReviewCommentCreatedPayload = WebhookSchemas["webhook-pull-request-review-comment-created"];
|
|
12
|
+
export type PullRequestReviewCommentEditedPayload = WebhookSchemas["webhook-pull-request-review-comment-edited"];
|
|
13
|
+
export type PullRequestReviewCommentDeletedPayload = WebhookSchemas["webhook-pull-request-review-comment-deleted"];
|
|
14
|
+
export type PullRequestReviewCommentPayload = PullRequestReviewCommentCreatedPayload | PullRequestReviewCommentEditedPayload | PullRequestReviewCommentDeletedPayload;
|
|
15
|
+
export type CheckSuiteCompletedPayload = WebhookSchemas["webhook-check-suite-completed"];
|
|
16
|
+
export type CheckSuiteRequestedPayload = WebhookSchemas["webhook-check-suite-requested"];
|
|
17
|
+
export type CheckSuiteRerequestedPayload = WebhookSchemas["webhook-check-suite-rerequested"];
|
|
18
|
+
export type CheckSuitePayload = CheckSuiteCompletedPayload | CheckSuiteRequestedPayload | CheckSuiteRerequestedPayload;
|
|
19
|
+
export type IssueCommentCreatedPayload = WebhookSchemas["webhook-issue-comment-created"];
|
|
20
|
+
export type IssueCommentEditedPayload = WebhookSchemas["webhook-issue-comment-edited"];
|
|
21
|
+
export type IssueCommentDeletedPayload = WebhookSchemas["webhook-issue-comment-deleted"];
|
|
22
|
+
export type IssueCommentPayload = IssueCommentCreatedPayload | IssueCommentEditedPayload | IssueCommentDeletedPayload;
|
|
23
|
+
export type SupportedWebhookPayload = WorkflowRunPayload | PullRequestReviewPayload | PullRequestReviewCommentPayload | CheckSuitePayload | IssueCommentPayload;
|
|
24
|
+
/**
|
|
25
|
+
* Fallback generic webhook payload structure for unknown or untyped events.
|
|
26
|
+
*/
|
|
27
|
+
export type WebhookPayload = Record<string, any>;
|
|
28
|
+
//# sourceMappingURL=payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/types/payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;AAGnD,MAAM,MAAM,2BAA2B,GACrC,cAAc,CAAC,gCAAgC,CAAC,CAAC;AACnD,MAAM,MAAM,4BAA4B,GACtC,cAAc,CAAC,kCAAkC,CAAC,CAAC;AACrD,MAAM,MAAM,2BAA2B,GACrC,cAAc,CAAC,gCAAgC,CAAC,CAAC;AACnD,MAAM,MAAM,kBAAkB,GAC1B,2BAA2B,GAC3B,4BAA4B,GAC5B,2BAA2B,CAAC;AAGhC,MAAM,MAAM,iCAAiC,GAC3C,cAAc,CAAC,uCAAuC,CAAC,CAAC;AAC1D,MAAM,MAAM,8BAA8B,GACxC,cAAc,CAAC,oCAAoC,CAAC,CAAC;AACvD,MAAM,MAAM,iCAAiC,GAC3C,cAAc,CAAC,uCAAuC,CAAC,CAAC;AAC1D,MAAM,MAAM,wBAAwB,GAChC,iCAAiC,GACjC,8BAA8B,GAC9B,iCAAiC,CAAC;AAGtC,MAAM,MAAM,sCAAsC,GAChD,cAAc,CAAC,6CAA6C,CAAC,CAAC;AAChE,MAAM,MAAM,qCAAqC,GAC/C,cAAc,CAAC,4CAA4C,CAAC,CAAC;AAC/D,MAAM,MAAM,sCAAsC,GAChD,cAAc,CAAC,6CAA6C,CAAC,CAAC;AAChE,MAAM,MAAM,+BAA+B,GACvC,sCAAsC,GACtC,qCAAqC,GACrC,sCAAsC,CAAC;AAG3C,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,+BAA+B,CAAC,CAAC;AAClD,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,+BAA+B,CAAC,CAAC;AAClD,MAAM,MAAM,4BAA4B,GACtC,cAAc,CAAC,iCAAiC,CAAC,CAAC;AACpD,MAAM,MAAM,iBAAiB,GACzB,0BAA0B,GAC1B,0BAA0B,GAC1B,4BAA4B,CAAC;AAGjC,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,+BAA+B,CAAC,CAAC;AAClD,MAAM,MAAM,yBAAyB,GACnC,cAAc,CAAC,8BAA8B,CAAC,CAAC;AACjD,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,+BAA+B,CAAC,CAAC;AAClD,MAAM,MAAM,mBAAmB,GAC3B,0BAA0B,GAC1B,yBAAyB,GACzB,0BAA0B,CAAC;AAE/B,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,wBAAwB,GACxB,+BAA+B,GAC/B,iBAAiB,GACjB,mBAAmB,CAAC;AAExB;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../src/types/payload.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
export declare const FilterSchema: Type.TObject<{
|
|
3
|
+
branch: Type.TOptional<Type.TString>;
|
|
4
|
+
conclusion: Type.TOptional<Type.TString>;
|
|
5
|
+
workflow: Type.TOptional<Type.TString>;
|
|
6
|
+
state: Type.TOptional<Type.TString>;
|
|
7
|
+
senders: Type.TOptional<Type.TArray<Type.TString>>;
|
|
8
|
+
isBot: Type.TOptional<Type.TBoolean>;
|
|
9
|
+
match: Type.TOptional<Type.TRecord<"^.*$", Type.TUnion<[Type.TString, Type.TNumber, Type.TBoolean]>>>;
|
|
10
|
+
}>;
|
|
11
|
+
export type FilterConfig = Static<typeof FilterSchema>;
|
|
12
|
+
export declare const ActionSchema: Type.TObject<{
|
|
13
|
+
prompt: Type.TString;
|
|
14
|
+
deliverAs: Type.TOptional<Type.TUnion<[Type.TLiteral<"steer">, Type.TLiteral<"followUp">]>>;
|
|
15
|
+
}>;
|
|
16
|
+
export type ActionConfig = Static<typeof ActionSchema>;
|
|
17
|
+
export declare const SubscriptionRuleSchema: Type.TObject<{
|
|
18
|
+
event: Type.TOptional<Type.TString>;
|
|
19
|
+
topic: Type.TOptional<Type.TString>;
|
|
20
|
+
filters: Type.TOptional<Type.TObject<{
|
|
21
|
+
branch: Type.TOptional<Type.TString>;
|
|
22
|
+
conclusion: Type.TOptional<Type.TString>;
|
|
23
|
+
workflow: Type.TOptional<Type.TString>;
|
|
24
|
+
state: Type.TOptional<Type.TString>;
|
|
25
|
+
senders: Type.TOptional<Type.TArray<Type.TString>>;
|
|
26
|
+
isBot: Type.TOptional<Type.TBoolean>;
|
|
27
|
+
match: Type.TOptional<Type.TRecord<"^.*$", Type.TUnion<[Type.TString, Type.TNumber, Type.TBoolean]>>>;
|
|
28
|
+
}>>;
|
|
29
|
+
action: Type.TObject<{
|
|
30
|
+
prompt: Type.TString;
|
|
31
|
+
deliverAs: Type.TOptional<Type.TUnion<[Type.TLiteral<"steer">, Type.TLiteral<"followUp">]>>;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
export type SubscriptionRule = Static<typeof SubscriptionRuleSchema>;
|
|
35
|
+
export declare const ConfigSchema: Type.TObject<{
|
|
36
|
+
version: Type.TLiteral<1>;
|
|
37
|
+
enabled: Type.TBoolean;
|
|
38
|
+
repo: Type.TOptional<Type.TString>;
|
|
39
|
+
secret: Type.TOptional<Type.TString>;
|
|
40
|
+
subscriptions: Type.TArray<Type.TObject<{
|
|
41
|
+
event: Type.TOptional<Type.TString>;
|
|
42
|
+
topic: Type.TOptional<Type.TString>;
|
|
43
|
+
filters: Type.TOptional<Type.TObject<{
|
|
44
|
+
branch: Type.TOptional<Type.TString>;
|
|
45
|
+
conclusion: Type.TOptional<Type.TString>;
|
|
46
|
+
workflow: Type.TOptional<Type.TString>;
|
|
47
|
+
state: Type.TOptional<Type.TString>;
|
|
48
|
+
senders: Type.TOptional<Type.TArray<Type.TString>>;
|
|
49
|
+
isBot: Type.TOptional<Type.TBoolean>;
|
|
50
|
+
match: Type.TOptional<Type.TRecord<"^.*$", Type.TUnion<[Type.TString, Type.TNumber, Type.TBoolean]>>>;
|
|
51
|
+
}>>;
|
|
52
|
+
action: Type.TObject<{
|
|
53
|
+
prompt: Type.TString;
|
|
54
|
+
deliverAs: Type.TOptional<Type.TUnion<[Type.TLiteral<"steer">, Type.TLiteral<"followUp">]>>;
|
|
55
|
+
}>;
|
|
56
|
+
}>>;
|
|
57
|
+
}>;
|
|
58
|
+
export type GhWebhookConfig = Static<typeof ConfigSchema>;
|
|
59
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/types/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,YAAY;;;;;;;;EAuCxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC;AAEvD,eAAO,MAAM,YAAY;;;EAaxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC;AAEvD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;EAkBlC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAErE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;EAkBxB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export const FilterSchema = Type.Object({
|
|
3
|
+
branch: Type.Optional(Type.String({ description: "Target branch name or '@current'" })),
|
|
4
|
+
conclusion: Type.Optional(Type.String({
|
|
5
|
+
description: "Conclusion filter (e.g. 'failure', 'success')",
|
|
6
|
+
})),
|
|
7
|
+
workflow: Type.Optional(Type.String({ description: "Workflow name filter (e.g. 'CI')" })),
|
|
8
|
+
state: Type.Optional(Type.String({
|
|
9
|
+
description: "Review or PR state filter (e.g. 'changes_requested', 'approved')",
|
|
10
|
+
})),
|
|
11
|
+
senders: Type.Optional(Type.Array(Type.String(), { description: "Allowed sender logins" })),
|
|
12
|
+
isBot: Type.Optional(Type.Boolean({
|
|
13
|
+
description: "Filter for bot (true) or human (false) senders",
|
|
14
|
+
})),
|
|
15
|
+
match: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number(), Type.Boolean()]), {
|
|
16
|
+
description: "Arbitrary field matching on payload (dot-separated path)",
|
|
17
|
+
})),
|
|
18
|
+
}, { additionalProperties: false });
|
|
19
|
+
export const ActionSchema = Type.Object({
|
|
20
|
+
prompt: Type.String({
|
|
21
|
+
description: "Template string to dispatch to agent",
|
|
22
|
+
}),
|
|
23
|
+
deliverAs: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("followUp")], {
|
|
24
|
+
description: "Delivery urgency: steer interrupts turn, followUp queues next",
|
|
25
|
+
})),
|
|
26
|
+
}, { additionalProperties: false });
|
|
27
|
+
export const SubscriptionRuleSchema = Type.Object({
|
|
28
|
+
event: Type.Optional(Type.String({
|
|
29
|
+
description: "Upstream GitHub Webhook event identifier (e.g. 'workflow_run.completed', 'push', 'issues.opened')",
|
|
30
|
+
})),
|
|
31
|
+
topic: Type.Optional(Type.String({
|
|
32
|
+
description: "Alias for 'event' (e.g. 'workflow_run.completed' or 'github.workflow_run.completed')",
|
|
33
|
+
})),
|
|
34
|
+
filters: Type.Optional(FilterSchema),
|
|
35
|
+
action: ActionSchema,
|
|
36
|
+
}, { additionalProperties: false });
|
|
37
|
+
export const ConfigSchema = Type.Object({
|
|
38
|
+
version: Type.Literal(1, { description: "Schema version" }),
|
|
39
|
+
enabled: Type.Boolean({ default: true }),
|
|
40
|
+
repo: Type.Optional(Type.String({
|
|
41
|
+
default: "auto",
|
|
42
|
+
description: "Target GitHub repo (owner/repo or 'auto')",
|
|
43
|
+
})),
|
|
44
|
+
secret: Type.Optional(Type.String({ description: "Optional Webhook HMAC secret" })),
|
|
45
|
+
subscriptions: Type.Array(SubscriptionRuleSchema, {
|
|
46
|
+
description: "List of declarative webhook event subscriptions",
|
|
47
|
+
}),
|
|
48
|
+
}, { additionalProperties: false });
|
|
49
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/types/schema.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ghwebhook",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal GitHub Webhook event bus and wake-up engine for Pi coding agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"extension",
|
|
11
|
+
"github",
|
|
12
|
+
"webhook",
|
|
13
|
+
"event-bus",
|
|
14
|
+
"automation"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/xifan2333/pi-ghwebhook#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/xifan2333/pi-ghwebhook/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/xifan2333/pi-ghwebhook.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "xifan",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"README_zh.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"check": "mise run check",
|
|
40
|
+
"prepublishOnly": "npm run check && npm test",
|
|
41
|
+
"test": "mise run test"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
45
|
+
"@octokit/openapi-webhooks-types": "^12.2.0",
|
|
46
|
+
"@types/node": "^22.13.0",
|
|
47
|
+
"typebox": "^1.3.27",
|
|
48
|
+
"typescript": "^5.8.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
52
|
+
"typebox": "*"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.19.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"pi": {
|
|
62
|
+
"extensions": [
|
|
63
|
+
"./dist/index.js"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SubscriptionRule } from "../types/schema.js";
|
|
2
|
+
import type { WebhookPayload } from "../types/payload.js";
|
|
3
|
+
import { evaluateGuards } from "./guards.js";
|
|
4
|
+
import { normalizeEventKey, resolveIncomingEventKeys } from "./normalizer.js";
|
|
5
|
+
import { extractVariables, renderTemplate } from "../template/renderer.js";
|
|
6
|
+
|
|
7
|
+
export interface ActionPlan {
|
|
8
|
+
prompt: string;
|
|
9
|
+
deliverAs: "steer" | "followUp";
|
|
10
|
+
event: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class WebhookEventBus {
|
|
14
|
+
private readonly routes = new Map<string, SubscriptionRule[]>();
|
|
15
|
+
|
|
16
|
+
constructor(rules: SubscriptionRule[]) {
|
|
17
|
+
for (const rule of rules) {
|
|
18
|
+
const rawKey = rule.event ?? rule.topic;
|
|
19
|
+
if (!rawKey) continue;
|
|
20
|
+
const normalizedKey = normalizeEventKey(rawKey);
|
|
21
|
+
|
|
22
|
+
const existing = this.routes.get(normalizedKey) ?? [];
|
|
23
|
+
existing.push(rule);
|
|
24
|
+
this.routes.set(normalizedKey, existing);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getEvents(): string[] {
|
|
29
|
+
return Array.from(this.routes.keys());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dispatch(
|
|
33
|
+
event: string,
|
|
34
|
+
payload: WebhookPayload,
|
|
35
|
+
currentBranch: string,
|
|
36
|
+
): ActionPlan[] {
|
|
37
|
+
const candidateKeys = resolveIncomingEventKeys(event, payload);
|
|
38
|
+
const seenRules = new Set<SubscriptionRule>();
|
|
39
|
+
const matchedRules: SubscriptionRule[] = [];
|
|
40
|
+
|
|
41
|
+
for (const key of candidateKeys) {
|
|
42
|
+
const rules = this.routes.get(key);
|
|
43
|
+
if (!rules) continue;
|
|
44
|
+
for (const rule of rules) {
|
|
45
|
+
if (!seenRules.has(rule)) {
|
|
46
|
+
seenRules.add(rule);
|
|
47
|
+
matchedRules.push(rule);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (matchedRules.length === 0) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ctx = { currentBranch, payload };
|
|
57
|
+
const vars = extractVariables(payload, currentBranch);
|
|
58
|
+
const plans: ActionPlan[] = [];
|
|
59
|
+
|
|
60
|
+
for (const rule of matchedRules) {
|
|
61
|
+
if (evaluateGuards(rule.filters, ctx)) {
|
|
62
|
+
plans.push({
|
|
63
|
+
prompt: renderTemplate(rule.action.prompt, vars),
|
|
64
|
+
deliverAs: rule.action.deliverAs ?? "followUp",
|
|
65
|
+
event: rule.event ?? rule.topic ?? event,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return plans;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { WebhookPayload } from "../types/payload.js";
|
|
2
|
+
import type { FilterConfig } from "../types/schema.js";
|
|
3
|
+
|
|
4
|
+
export interface FilterContext {
|
|
5
|
+
currentBranch: string;
|
|
6
|
+
payload: WebhookPayload;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type GuardPredicate = (
|
|
10
|
+
expectedValue: unknown,
|
|
11
|
+
ctx: FilterContext,
|
|
12
|
+
) => boolean;
|
|
13
|
+
|
|
14
|
+
function resolvePayloadBranch(payload: WebhookPayload): string | undefined {
|
|
15
|
+
return (
|
|
16
|
+
payload.workflow_run?.head_branch ??
|
|
17
|
+
payload.pull_request?.head?.ref ??
|
|
18
|
+
payload.check_suite?.head_branch ??
|
|
19
|
+
payload.workflow_job?.head_branch ??
|
|
20
|
+
(typeof payload.ref === "string"
|
|
21
|
+
? payload.ref.replace("refs/heads/", "")
|
|
22
|
+
: undefined)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolvePayloadSender(payload: WebhookPayload): string | undefined {
|
|
27
|
+
return (
|
|
28
|
+
payload.sender?.login ??
|
|
29
|
+
payload.review?.user?.login ??
|
|
30
|
+
payload.comment?.user?.login
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getNestedValue(obj: Record<string, any>, path: string): unknown {
|
|
35
|
+
const parts = path.split(".");
|
|
36
|
+
let current: any = obj;
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
39
|
+
current = current[part];
|
|
40
|
+
}
|
|
41
|
+
return current;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const branchGuard: GuardPredicate = (expected, { currentBranch, payload }) => {
|
|
45
|
+
const target = expected === "@current" ? currentBranch : expected;
|
|
46
|
+
const actualBranch = resolvePayloadBranch(payload);
|
|
47
|
+
return typeof target === "string" && actualBranch === target;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const conclusionGuard: GuardPredicate = (expected, { payload }) => {
|
|
51
|
+
const actual =
|
|
52
|
+
payload.workflow_run?.conclusion ??
|
|
53
|
+
payload.check_suite?.conclusion ??
|
|
54
|
+
payload.workflow_job?.conclusion;
|
|
55
|
+
return typeof expected === "string" && actual === expected;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const workflowGuard: GuardPredicate = (expected, { payload }) => {
|
|
59
|
+
const workflowName =
|
|
60
|
+
payload.workflow_run?.name ?? payload.workflow_job?.workflow_name;
|
|
61
|
+
return typeof expected === "string" && workflowName === expected;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const stateGuard: GuardPredicate = (expected, { payload }) => {
|
|
65
|
+
const actual = payload.review?.state ?? payload.pull_request?.state;
|
|
66
|
+
return typeof expected === "string" && actual === expected;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sendersGuard: GuardPredicate = (allowedList, { payload }) => {
|
|
70
|
+
if (!Array.isArray(allowedList)) return true;
|
|
71
|
+
const sender = resolvePayloadSender(payload);
|
|
72
|
+
return typeof sender === "string" && allowedList.includes(sender);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isBotGuard: GuardPredicate = (expected, { payload }) => {
|
|
76
|
+
const senderType = payload.review?.user?.type ?? payload.sender?.type;
|
|
77
|
+
const login = payload.review?.user?.login ?? payload.sender?.login ?? "";
|
|
78
|
+
const isBot = senderType === "Bot" || login.endsWith("[bot]");
|
|
79
|
+
return isBot === Boolean(expected);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const matchGuard: GuardPredicate = (matchObj, { payload }) => {
|
|
83
|
+
if (!matchObj || typeof matchObj !== "object") return true;
|
|
84
|
+
for (const [path, expected] of Object.entries(
|
|
85
|
+
matchObj as Record<string, unknown>,
|
|
86
|
+
)) {
|
|
87
|
+
const actual = getNestedValue(payload, path);
|
|
88
|
+
if (actual !== expected) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const GUARDS: ReadonlyMap<keyof FilterConfig, GuardPredicate> = new Map([
|
|
94
|
+
["branch", branchGuard],
|
|
95
|
+
["conclusion", conclusionGuard],
|
|
96
|
+
["workflow", workflowGuard],
|
|
97
|
+
["state", stateGuard],
|
|
98
|
+
["senders", sendersGuard],
|
|
99
|
+
["isBot", isBotGuard],
|
|
100
|
+
["match", matchGuard],
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
export function evaluateGuards(
|
|
104
|
+
filters: FilterConfig | undefined,
|
|
105
|
+
ctx: FilterContext,
|
|
106
|
+
): boolean {
|
|
107
|
+
if (!filters) return true;
|
|
108
|
+
|
|
109
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
110
|
+
if (value === undefined) continue;
|
|
111
|
+
const guard = GUARDS.get(key as keyof FilterConfig);
|
|
112
|
+
if (guard && !guard(value, ctx)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WebhookPayload } from "../types/payload.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes an event identifier by stripping optional "github." prefix.
|
|
5
|
+
* e.g. "github.workflow_run.completed" -> "workflow_run.completed"
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeEventKey(key: string): string {
|
|
8
|
+
const trimmed = key.trim();
|
|
9
|
+
return trimmed.startsWith("github.") ? trimmed.slice(7) : trimmed;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves candidate event keys from an incoming GitHub Webhook event and payload.
|
|
14
|
+
* Upstream convention:
|
|
15
|
+
* 1. If payload has action: `${event}.${action}` (e.g. "workflow_run.completed", "pull_request.opened")
|
|
16
|
+
* 2. Wildcard/base event: `${event}` (e.g. "workflow_run", "push", "issues")
|
|
17
|
+
*/
|
|
18
|
+
export function resolveIncomingEventKeys(
|
|
19
|
+
event: string,
|
|
20
|
+
payload: WebhookPayload,
|
|
21
|
+
): string[] {
|
|
22
|
+
const normalizedEvent = normalizeEventKey(event);
|
|
23
|
+
const action =
|
|
24
|
+
typeof payload.action === "string" ? payload.action.trim() : undefined;
|
|
25
|
+
|
|
26
|
+
const keys: string[] = [];
|
|
27
|
+
if (action) {
|
|
28
|
+
keys.push(`${normalizedEvent}.${action}`);
|
|
29
|
+
}
|
|
30
|
+
keys.push(normalizedEvent);
|
|
31
|
+
return keys;
|
|
32
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFile, access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import type { GhWebhookConfig } from "../types/schema.js";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
const CONFIG_CANDIDATES = [
|
|
10
|
+
".pi/ghwebhook.json",
|
|
11
|
+
".agents/ghwebhook.json",
|
|
12
|
+
"ghwebhook.json",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
await access(path);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function loadProjectConfig(
|
|
25
|
+
cwd: string,
|
|
26
|
+
): Promise<GhWebhookConfig | null> {
|
|
27
|
+
for (const relPath of CONFIG_CANDIDATES) {
|
|
28
|
+
const fullPath = join(cwd, relPath);
|
|
29
|
+
if (await fileExists(fullPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const content = await readFile(fullPath, "utf8");
|
|
32
|
+
const parsed = JSON.parse(content) as GhWebhookConfig;
|
|
33
|
+
if (parsed && typeof parsed === "object" && parsed.version === 1) {
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(
|
|
38
|
+
`[pi-ghwebhook] Failed to load config from ${fullPath}:`,
|
|
39
|
+
err,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function resolveGitBranch(cwd: string): Promise<string> {
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await execFileAsync(
|
|
50
|
+
"git",
|
|
51
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
52
|
+
{ cwd },
|
|
53
|
+
);
|
|
54
|
+
return stdout.trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return "main";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseGitHubRepo(remoteUrl: string): string | null {
|
|
61
|
+
const normalized = remoteUrl.trim();
|
|
62
|
+
const match = normalized.match(
|
|
63
|
+
/github\.com[:/]([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?$/i,
|
|
64
|
+
);
|
|
65
|
+
if (!match) return null;
|
|
66
|
+
return `${match[1]}/${match[2]}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function resolveGitRepo(cwd: string): Promise<string | null> {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await execFileAsync(
|
|
72
|
+
"git",
|
|
73
|
+
["remote", "get-url", "origin"],
|
|
74
|
+
{ cwd },
|
|
75
|
+
);
|
|
76
|
+
return parseGitHubRepo(stdout);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
loadProjectConfig,
|
|
4
|
+
resolveGitBranch,
|
|
5
|
+
resolveGitRepo,
|
|
6
|
+
} from "./config/loader.js";
|
|
7
|
+
import { WebhookEventBus } from "./bus/event-bus.js";
|
|
8
|
+
import { LoopbackServer } from "./transport/loopback.js";
|
|
9
|
+
import { GhForwarder } from "./transport/forwarder.js";
|
|
10
|
+
|
|
11
|
+
export default function ghWebhookExtension(pi: ExtensionAPI): void {
|
|
12
|
+
let loopback: LoopbackServer | null = null;
|
|
13
|
+
let forwarder: GhForwarder | null = null;
|
|
14
|
+
|
|
15
|
+
const cleanup = async () => {
|
|
16
|
+
if (forwarder) {
|
|
17
|
+
forwarder.stop();
|
|
18
|
+
forwarder = null;
|
|
19
|
+
}
|
|
20
|
+
if (loopback) {
|
|
21
|
+
await loopback.stop();
|
|
22
|
+
loopback = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
process.once("exit", () => {
|
|
27
|
+
if (forwarder) forwarder.stop();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
31
|
+
// 1. 工作区探测:若当前项目无 .pi/ghwebhook.json,则静默休眠,零资源占用
|
|
32
|
+
const config = await loadProjectConfig(ctx.cwd);
|
|
33
|
+
if (!config || !config.enabled) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. 解析当前 Git 分支与远端仓库
|
|
38
|
+
const currentBranch = await resolveGitBranch(ctx.cwd);
|
|
39
|
+
const repo =
|
|
40
|
+
config.repo && config.repo !== "auto"
|
|
41
|
+
? config.repo
|
|
42
|
+
: await resolveGitRepo(ctx.cwd);
|
|
43
|
+
|
|
44
|
+
if (!repo) {
|
|
45
|
+
if (ctx.hasUI) {
|
|
46
|
+
ctx.ui.notify(
|
|
47
|
+
"[pi-ghwebhook] 未能检测到当前项目的 GitHub 仓库 (owner/repo),请在配置中显式指定 'repo' 字段。",
|
|
48
|
+
"warning",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. 构建表驱动事件总线
|
|
55
|
+
const bus = new WebhookEventBus(config.subscriptions);
|
|
56
|
+
|
|
57
|
+
// 4. 启动随机端口本地回环服务器
|
|
58
|
+
loopback = new LoopbackServer({
|
|
59
|
+
secret: config.secret,
|
|
60
|
+
onEvent: async (event, payload) => {
|
|
61
|
+
const plans = bus.dispatch(event, payload, currentBranch);
|
|
62
|
+
for (const plan of plans) {
|
|
63
|
+
pi.sendUserMessage(plan.prompt, {
|
|
64
|
+
deliverAs: plan.deliverAs,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await loopback.start();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error("[pi-ghwebhook] 无法启动回环服务:", err);
|
|
74
|
+
if (ctx.hasUI) {
|
|
75
|
+
ctx.ui.notify(
|
|
76
|
+
"[pi-ghwebhook] 无法启动回环服务,事件流未能挂载",
|
|
77
|
+
"error",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. 托管 gh webhook forward 长连接子进程(默认全量监听 --events=*)
|
|
84
|
+
forwarder = new GhForwarder({
|
|
85
|
+
repo,
|
|
86
|
+
url: loopback.getUrl(),
|
|
87
|
+
secret: config.secret,
|
|
88
|
+
onError: (err) => {
|
|
89
|
+
console.error("[pi-ghwebhook] gh forwarder error:", err);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
forwarder.start();
|
|
94
|
+
|
|
95
|
+
if (ctx.hasUI) {
|
|
96
|
+
ctx.ui.notify(
|
|
97
|
+
`[pi-ghwebhook] 已监听 ${repo} (${currentBranch}) GitHub 事件流 (端口: ${loopback.getPort()})`,
|
|
98
|
+
"info",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
pi.on("session_shutdown", async () => {
|
|
104
|
+
await cleanup();
|
|
105
|
+
});
|
|
106
|
+
}
|