pi-ghwebhook 0.1.0 → 0.2.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.
@@ -0,0 +1,225 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/xifan2333/pi-ghwebhook/main/schemas/ghwebhook.schema.json",
4
+ "title": "Pi GitHub Webhook Workflow Configuration",
5
+ "description": "GitHub Actions Workflow-compatible event automation configuration for Pi coding agent",
6
+ "type": "object",
7
+ "required": ["on", "jobs"],
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "URL of the JSON schema for validation and autocompletion"
12
+ },
13
+ "name": {
14
+ "type": "string",
15
+ "description": "Optional display name for this event automation workflow"
16
+ },
17
+ "repo": {
18
+ "type": "string",
19
+ "default": "auto",
20
+ "description": "Target GitHub repository in 'owner/repo' format, or 'auto' to detect from git remote"
21
+ },
22
+ "secret": {
23
+ "type": "string",
24
+ "description": "Optional Webhook HMAC SHA-256 verification secret"
25
+ },
26
+ "on": {
27
+ "description": "GitHub event triggers, matching GitHub Actions 'on' specification",
28
+ "oneOf": [
29
+ {
30
+ "type": "string",
31
+ "description": "Single GitHub event name, e.g. 'push' or 'workflow_run'"
32
+ },
33
+ {
34
+ "type": "array",
35
+ "items": { "type": "string" },
36
+ "description": "List of GitHub event names, e.g. ['push', 'pull_request']"
37
+ },
38
+ {
39
+ "type": "object",
40
+ "description": "Map of event names to their trigger filtering criteria",
41
+ "properties": {
42
+ "workflow_run": {
43
+ "type": "object",
44
+ "properties": {
45
+ "types": {
46
+ "type": "array",
47
+ "items": {
48
+ "type": "string",
49
+ "enum": ["completed", "requested", "in_progress"]
50
+ },
51
+ "description": "Activity types that trigger the workflow"
52
+ },
53
+ "branches": {
54
+ "type": "array",
55
+ "items": { "type": "string" },
56
+ "description": "Branches to filter on (supports '@current' and globs like 'main', 'feat/*')"
57
+ },
58
+ "workflows": {
59
+ "type": "array",
60
+ "items": { "type": "string" },
61
+ "description": "Workflow names to filter on (e.g. ['CI'])"
62
+ }
63
+ },
64
+ "additionalProperties": false
65
+ },
66
+ "pull_request": {
67
+ "type": "object",
68
+ "properties": {
69
+ "types": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "string",
73
+ "enum": [
74
+ "opened",
75
+ "edited",
76
+ "closed",
77
+ "reopened",
78
+ "synchronize",
79
+ "ready_for_review"
80
+ ]
81
+ }
82
+ },
83
+ "branches": {
84
+ "type": "array",
85
+ "items": { "type": "string" }
86
+ }
87
+ },
88
+ "additionalProperties": false
89
+ },
90
+ "pull_request_review": {
91
+ "type": "object",
92
+ "properties": {
93
+ "types": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "string",
97
+ "enum": ["submitted", "edited", "dismissed"]
98
+ }
99
+ },
100
+ "branches": {
101
+ "type": "array",
102
+ "items": { "type": "string" }
103
+ }
104
+ },
105
+ "additionalProperties": false
106
+ },
107
+ "pull_request_review_comment": {
108
+ "type": "object",
109
+ "properties": {
110
+ "types": {
111
+ "type": "array",
112
+ "items": {
113
+ "type": "string",
114
+ "enum": ["created", "edited", "deleted"]
115
+ }
116
+ }
117
+ },
118
+ "additionalProperties": false
119
+ },
120
+ "check_suite": {
121
+ "type": "object",
122
+ "properties": {
123
+ "types": {
124
+ "type": "array",
125
+ "items": {
126
+ "type": "string",
127
+ "enum": ["completed", "requested", "rerequested"]
128
+ }
129
+ },
130
+ "branches": {
131
+ "type": "array",
132
+ "items": { "type": "string" }
133
+ }
134
+ },
135
+ "additionalProperties": false
136
+ },
137
+ "issues": {
138
+ "type": "object",
139
+ "properties": {
140
+ "types": {
141
+ "type": "array",
142
+ "items": {
143
+ "type": "string",
144
+ "enum": [
145
+ "opened",
146
+ "edited",
147
+ "deleted",
148
+ "closed",
149
+ "reopened",
150
+ "assigned",
151
+ "labeled"
152
+ ]
153
+ }
154
+ }
155
+ },
156
+ "additionalProperties": false
157
+ },
158
+ "issue_comment": {
159
+ "type": "object",
160
+ "properties": {
161
+ "types": {
162
+ "type": "array",
163
+ "items": {
164
+ "type": "string",
165
+ "enum": ["created", "edited", "deleted"]
166
+ }
167
+ }
168
+ },
169
+ "additionalProperties": false
170
+ },
171
+ "push": {
172
+ "type": "object",
173
+ "properties": {
174
+ "branches": {
175
+ "type": "array",
176
+ "items": { "type": "string" }
177
+ }
178
+ },
179
+ "additionalProperties": false
180
+ }
181
+ },
182
+ "additionalProperties": {
183
+ "type": "object",
184
+ "properties": {
185
+ "types": { "type": "array", "items": { "type": "string" } },
186
+ "branches": { "type": "array", "items": { "type": "string" } },
187
+ "workflows": { "type": "array", "items": { "type": "string" } }
188
+ },
189
+ "additionalProperties": true
190
+ }
191
+ }
192
+ ]
193
+ },
194
+ "jobs": {
195
+ "type": "object",
196
+ "description": "Map of jobs to execute when matching events arrive",
197
+ "additionalProperties": {
198
+ "type": "object",
199
+ "required": ["prompt"],
200
+ "properties": {
201
+ "name": {
202
+ "type": "string",
203
+ "description": "Optional job title"
204
+ },
205
+ "if": {
206
+ "type": "string",
207
+ "description": "Conditional expression, e.g. \"github.event.workflow_run.conclusion == 'failure'\""
208
+ },
209
+ "prompt": {
210
+ "type": "string",
211
+ "description": "Prompt template sent to Pi Agent, supporting {{ branch }}, {{ run_url }}, etc."
212
+ },
213
+ "deliverAs": {
214
+ "type": "string",
215
+ "enum": ["followUp", "steer"],
216
+ "default": "followUp",
217
+ "description": "Pi message delivery mode: 'followUp' (wait for agent to finish) or 'steer' (interrupt turn)"
218
+ }
219
+ },
220
+ "additionalProperties": false
221
+ }
222
+ }
223
+ },
224
+ "additionalProperties": false
225
+ }
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: setup-ghwebhook
3
+ description: Set up and configure declarative GitHub Webhook automation (.pi/ghwebhook.json) for the current repository. Use when configuring CI/CD event-driven agent triggers, setting up webhook forwarders, or enabling automated CI-fix and PR-review loops in Pi.
4
+ ---
5
+
6
+ # Setup GitHub Webhook Automation
7
+
8
+ Configure zero-polling, event-driven GitHub Webhook automation for the current repository using the official GitHub Actions Workflow specification (`on` + `jobs`). When events occur (such as CI failures or review submissions), `pi-ghwebhook` intercepts them via a local reverse tunnel and wakes up the agent via `pi.sendUserMessage()` to close the loop automatically.
9
+
10
+ ## Work Loop
11
+
12
+ Follow these steps in sequence. Do not proceed to the next step until the current step satisfies its completion criterion.
13
+
14
+ ### Step 1: Verify prerequisites and auto-heal
15
+
16
+ 1. Check GitHub CLI installation and authentication:
17
+ ```bash
18
+ gh auth status
19
+ ```
20
+ If unauthenticated, instruct the human to log in with `gh auth login`.
21
+ 2. Check if the official GitHub Webhook extension is installed:
22
+ ```bash
23
+ gh extension list
24
+ ```
25
+ If `cli/gh-webhook` (or `gh-webhook`) is missing, install it immediately:
26
+ ```bash
27
+ gh extension install cli/gh-webhook
28
+ ```
29
+ 3. Verify the current working directory is a Git repository with a GitHub remote:
30
+ ```bash
31
+ git remote get-url origin
32
+ ```
33
+ Ensure the remote URL points to a GitHub repository (`github.com`).
34
+
35
+ _Completion criterion_: `gh` is authenticated, `cli/gh-webhook` extension is verified installed, and the repository remote is confirmed on GitHub.
36
+
37
+ ### Step 2: Sense repository context
38
+
39
+ Inspect the repository to tailor the subscription rules:
40
+
41
+ 1. Discover GitHub Actions workflows under `.github/workflows/`:
42
+ ```bash
43
+ ls .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true
44
+ ```
45
+ Inspect workflow names (such as `CI`, `Build`, `Test`, `Release`). If an obvious main CI workflow exists, record its name for the `workflows` filter.
46
+ 2. Check for AI code review bot integrations:
47
+ - Check if `.coderabbit.yaml` or `.coderabbit.yml` exists.
48
+ - Check for Greptile, Copilot, or other review bot configurations.
49
+
50
+ _Completion criterion_: Identified primary CI workflow name (or determined to match all workflows) and identified whether review bot subscriptions are needed.
51
+
52
+ ### Step 3: Scaffold `.pi/ghwebhook.json`
53
+
54
+ Write `.pi/ghwebhook.json` in the repository root. Include the `$schema` property for IDE autocompletion and validation:
55
+
56
+ ```json
57
+ {
58
+ "$schema": "https://raw.githubusercontent.com/xifan2333/pi-ghwebhook/main/schemas/ghwebhook.schema.json",
59
+ "name": "CI & Review Automation",
60
+ "on": {
61
+ "workflow_run": {
62
+ "types": ["completed"],
63
+ "branches": ["@current"]
64
+ },
65
+ "pull_request_review": {
66
+ "types": ["submitted"],
67
+ "branches": ["@current"]
68
+ }
69
+ },
70
+ "jobs": {
71
+ "ci-failure": {
72
+ "if": "github.event.workflow_run.conclusion == 'failure'",
73
+ "prompt": "CI run failed on branch {{ branch }}.\n\nRun URL: {{ run_url }}\nCommit: {{ head_sha }}\n\nPlease run `gh run view {{ run_id }} --log-failed` to diagnose the error and prepare a fix.",
74
+ "deliverAs": "followUp"
75
+ },
76
+ "ci-success": {
77
+ "if": "github.event.workflow_run.conclusion == 'success'",
78
+ "prompt": "Remote CI has passed on branch {{ branch }}.\n\nRun URL: {{ run_url }}\n\nPlease check PR checks status and verify if it is ready to merge.",
79
+ "deliverAs": "followUp"
80
+ },
81
+ "review-bot": {
82
+ "if": "github.event.review.user.type == 'Bot'",
83
+ "prompt": "Received review comments from {{ sender }}:\n\nState: {{ review_state }}\nDetails:\n{{ review_body }}\n\nPlease evaluate these comments critically: triage true issues to fix, and refute false positives.",
84
+ "deliverAs": "steer"
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ If specific workflows were identified in Step 2 (e.g. `CI`), add `"workflows": ["CI"]` under `on.workflow_run`.
91
+
92
+ _Completion criterion_: `.pi/ghwebhook.json` exists in repository root, references `$schema`, is valid JSON, and contains tailored `on` and `jobs`.
93
+
94
+ ### Step 4: Verify and activate
95
+
96
+ 1. If running inside an active interactive Pi session, instruct the user to reload:
97
+ ```text
98
+ /reload
99
+ ```
100
+ 2. On session start, `pi-ghwebhook` automatically detects `.pi/ghwebhook.json`, binds an ephemeral local port on `127.0.0.1:0`, spawns `gh webhook forward --events=*`, and displays an info notification in TUI.
101
+
102
+ _Completion criterion_: Explained activation method to the user and confirmed configuration ready for incoming events.
103
+
104
+ ---
105
+
106
+ ## Reference
107
+
108
+ ### Schema and Autocompletion
109
+
110
+ Add `$schema` at the top of `.pi/ghwebhook.json`:
111
+
112
+ ```json
113
+ "$schema": "https://raw.githubusercontent.com/xifan2333/pi-ghwebhook/main/schemas/ghwebhook.schema.json"
114
+ ```
115
+
116
+ ### Supported Trigger Keys under `on.<event>`
117
+
118
+ - `types`: Activity types filtering `payload.action` (e.g. `["completed"]`, `["submitted"]`, `["opened"]`).
119
+ - `branches`: Branches filter supporting `@current` and globs (e.g. `["@current", "main", "feat/*"]`).
120
+ - `workflows`: Workflow names filter (e.g. `["CI"]`).
121
+
122
+ ### Conditions under `jobs.<job_id>.if`
123
+
124
+ Supports standard comparison expressions without arbitrary code execution:
125
+
126
+ - `github.event.workflow_run.conclusion == 'failure'`
127
+ - `github.event.workflow_run.conclusion == 'success'`
128
+ - `github.event.review.user.type == 'Bot'`
129
+ - `github.event.sender.login == 'coderabbitai[bot]'`
130
+ - Glob matching: `github.event.sender.login == '*[bot]'`
131
+
132
+ ### Template Variables
133
+
134
+ - `{{ branch }}`: Current head branch.
135
+ - `{{ head_sha }}`: Commit SHA.
136
+ - `{{ commit_message }}`: Head commit message.
137
+ - `{{ repo }}`: Repository full name (`owner/repo`).
138
+ - `{{ repo_url }}`: Repository HTML URL.
139
+ - `{{ run_id }}`: Workflow run ID.
140
+ - `{{ run_url }}`: Workflow run HTML URL.
141
+ - `{{ workflow }}`: Workflow name.
142
+ - `{{ conclusion }}`: Workflow conclusion.
143
+ - `{{ pr_number }}`: Pull request number.
144
+ - `{{ pr_title }}`: Pull request title.
145
+ - `{{ pr_url }}`: Pull request HTML URL.
146
+ - `{{ review_state }}`: Review state (`approved`, `changes_requested`, `commented`).
147
+ - `{{ review_body }}`: Review body text.
148
+ - `{{ sender }}`: Actor username or bot login.
149
+ - `{{ action }}`: Raw event action string.
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/xifan2333/pi-ghwebhook/main/schemas/ghwebhook.schema.json",
3
+ "name": "CI & Review Automation",
4
+ "on": {
5
+ "workflow_run": {
6
+ "types": ["completed"],
7
+ "branches": ["@current"]
8
+ },
9
+ "pull_request_review": {
10
+ "types": ["submitted"],
11
+ "branches": ["@current"]
12
+ }
13
+ },
14
+ "jobs": {
15
+ "ci-failure": {
16
+ "if": "github.event.workflow_run.conclusion == 'failure'",
17
+ "prompt": "CI run failed on branch {{ branch }}.\n\nRun URL: {{ run_url }}\nCommit: {{ head_sha }}\n\nPlease run `gh run view {{ run_id }} --log-failed` to diagnose the error and prepare a fix.",
18
+ "deliverAs": "followUp"
19
+ },
20
+ "ci-success": {
21
+ "if": "github.event.workflow_run.conclusion == 'success'",
22
+ "prompt": "Remote CI has passed on branch {{ branch }}.\n\nRun URL: {{ run_url }}\n\nPlease check PR checks status and verify if it is ready to merge.",
23
+ "deliverAs": "followUp"
24
+ },
25
+ "review-bot": {
26
+ "if": "github.event.review.user.type == 'Bot'",
27
+ "prompt": "Received review comments from {{ sender }}:\n\nState: {{ review_state }}\nDetails:\n{{ review_body }}\n\nPlease evaluate these comments critically: triage true issues to fix, and refute false positives.",
28
+ "deliverAs": "steer"
29
+ }
30
+ }
31
+ }
@@ -1,70 +1,62 @@
1
- import type { SubscriptionRule } from "../types/schema.js";
1
+ import type { GhWebhookWorkflow, EventTriggerConfig } from "../types/schema.js";
2
2
  import type { WebhookPayload } from "../types/payload.js";
3
- import { evaluateGuards } from "./guards.js";
4
- import { normalizeEventKey, resolveIncomingEventKeys } from "./normalizer.js";
3
+ import { isTriggerMatched, evaluateCondition } from "./matcher.js";
5
4
  import { extractVariables, renderTemplate } from "../template/renderer.js";
6
5
 
7
6
  export interface ActionPlan {
8
7
  prompt: string;
9
8
  deliverAs: "steer" | "followUp";
10
- event: string;
9
+ jobId: string;
11
10
  }
12
11
 
13
12
  export class WebhookEventBus {
14
- private readonly routes = new Map<string, SubscriptionRule[]>();
13
+ constructor(private readonly workflow: GhWebhookWorkflow) {}
15
14
 
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);
15
+ private getTriggerForEvent(
16
+ incomingEvent: string,
17
+ ): EventTriggerConfig | null | undefined {
18
+ const on = this.workflow.on;
19
+ if (typeof on === "string") {
20
+ return on === incomingEvent ? null : undefined;
25
21
  }
26
- }
27
-
28
- getEvents(): string[] {
29
- return Array.from(this.routes.keys());
22
+ if (Array.isArray(on)) {
23
+ return on.includes(incomingEvent) ? null : undefined;
24
+ }
25
+ if (typeof on === "object" && on !== null) {
26
+ if (Object.prototype.hasOwnProperty.call(on, incomingEvent)) {
27
+ return on[incomingEvent] as EventTriggerConfig | null;
28
+ }
29
+ }
30
+ return undefined;
30
31
  }
31
32
 
32
33
  dispatch(
33
- event: string,
34
+ incomingEvent: string,
34
35
  payload: WebhookPayload,
35
36
  currentBranch: string,
36
37
  ): 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
- }
38
+ const trigger = this.getTriggerForEvent(incomingEvent);
39
+ if (trigger === undefined) {
40
+ return [];
50
41
  }
51
42
 
52
- if (matchedRules.length === 0) {
43
+ if (!isTriggerMatched(trigger, payload, currentBranch)) {
53
44
  return [];
54
45
  }
55
46
 
56
- const ctx = { currentBranch, payload };
57
47
  const vars = extractVariables(payload, currentBranch);
58
48
  const plans: ActionPlan[] = [];
59
49
 
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
- });
50
+ for (const [jobId, job] of Object.entries(this.workflow.jobs)) {
51
+ if (!evaluateCondition(job.if, payload, currentBranch)) {
52
+ continue;
67
53
  }
54
+
55
+ plans.push({
56
+ prompt: renderTemplate(job.prompt, vars),
57
+ deliverAs: job.deliverAs ?? "followUp",
58
+ jobId,
59
+ });
68
60
  }
69
61
 
70
62
  return plans;
@@ -0,0 +1,164 @@
1
+ import type { WebhookPayload } from "../types/payload.js";
2
+ import type { EventTriggerConfig } from "../types/schema.js";
3
+
4
+ export function globMatch(pattern: string, text: string): boolean {
5
+ if (pattern === text || pattern === "*") return true;
6
+ const escaped = pattern
7
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
8
+ .replace(/\*\*/g, ".*")
9
+ .replace(/\*/g, ".*");
10
+ const regex = new RegExp(`^${escaped}$`);
11
+ return regex.test(text);
12
+ }
13
+
14
+ export function resolvePayloadBranch(
15
+ payload: WebhookPayload,
16
+ ): string | undefined {
17
+ return (
18
+ payload.workflow_run?.head_branch ??
19
+ payload.pull_request?.head?.ref ??
20
+ payload.check_suite?.head_branch ??
21
+ payload.workflow_job?.head_branch ??
22
+ (typeof payload.ref === "string"
23
+ ? payload.ref.replace("refs/heads/", "")
24
+ : undefined)
25
+ );
26
+ }
27
+
28
+ function matchBranches(
29
+ branches: string[],
30
+ actualBranch: string,
31
+ currentBranch: string,
32
+ ): boolean {
33
+ for (const pattern of branches) {
34
+ if (pattern === "@current" && actualBranch === currentBranch) {
35
+ return true;
36
+ }
37
+ if (globMatch(pattern, actualBranch)) {
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function matchTypes(types: string[] | undefined, action: unknown): boolean {
45
+ if (!Array.isArray(types) || types.length === 0) return true;
46
+ const actionStr = typeof action === "string" ? action : "";
47
+ return types.includes(actionStr);
48
+ }
49
+
50
+ function matchBranchFilter(
51
+ branches: string[] | undefined,
52
+ payload: WebhookPayload,
53
+ currentBranch: string,
54
+ ): boolean {
55
+ if (!Array.isArray(branches) || branches.length === 0) return true;
56
+ const actualBranch = resolvePayloadBranch(payload);
57
+ return Boolean(
58
+ actualBranch && matchBranches(branches, actualBranch, currentBranch),
59
+ );
60
+ }
61
+
62
+ function matchWorkflows(
63
+ workflows: string[] | undefined,
64
+ payload: WebhookPayload,
65
+ ): boolean {
66
+ if (!Array.isArray(workflows) || workflows.length === 0) return true;
67
+ const workflowName =
68
+ payload.workflow_run?.name ?? payload.workflow_job?.workflow_name ?? "";
69
+ return workflows.some((w) => globMatch(w, workflowName));
70
+ }
71
+
72
+ export function isTriggerMatched(
73
+ trigger: EventTriggerConfig | null | undefined,
74
+ payload: WebhookPayload,
75
+ currentBranch: string,
76
+ ): boolean {
77
+ if (!trigger) return true;
78
+ return (
79
+ matchTypes(trigger.types, payload.action) &&
80
+ matchBranchFilter(trigger.branches, payload, currentBranch) &&
81
+ matchWorkflows(trigger.workflows, payload)
82
+ );
83
+ }
84
+
85
+ function getNestedValue(obj: Record<string, any>, path: string): unknown {
86
+ const parts = path.split(".");
87
+ let current: any = obj;
88
+ for (const part of parts) {
89
+ if (current == null || typeof current !== "object") return undefined;
90
+ current = current[part];
91
+ }
92
+ return current;
93
+ }
94
+
95
+ function resolveContextValue(
96
+ path: string,
97
+ payload: WebhookPayload,
98
+ currentBranch: string,
99
+ ): unknown {
100
+ const cleanPath = path
101
+ .replace(/^github\.event\./, "")
102
+ .replace(/^event\./, "")
103
+ .trim();
104
+
105
+ // Special shortcuts
106
+ if (cleanPath === "branch")
107
+ return resolvePayloadBranch(payload) ?? currentBranch;
108
+ if (cleanPath === "conclusion")
109
+ return payload.workflow_run?.conclusion ?? payload.check_suite?.conclusion;
110
+ if (cleanPath === "sender")
111
+ return payload.sender?.login ?? payload.review?.user?.login;
112
+ if (cleanPath === "isBot") {
113
+ const senderType = payload.review?.user?.type ?? payload.sender?.type;
114
+ const login = payload.review?.user?.login ?? payload.sender?.login ?? "";
115
+ return senderType === "Bot" || login.endsWith("[bot]");
116
+ }
117
+
118
+ return getNestedValue(payload, cleanPath);
119
+ }
120
+
121
+ function parseLiteral(literal: string): string | number | boolean | null {
122
+ const trimmed = literal.trim();
123
+ if (trimmed === "true") return true;
124
+ if (trimmed === "false") return false;
125
+ if (trimmed === "null") return null;
126
+ if (/^['"].*['"]$/.test(trimmed)) {
127
+ return trimmed.slice(1, -1);
128
+ }
129
+ const num = Number(trimmed);
130
+ if (!Number.isNaN(num)) return num;
131
+ return trimmed;
132
+ }
133
+
134
+ export function evaluateCondition(
135
+ expr: string | undefined,
136
+ payload: WebhookPayload,
137
+ currentBranch: string,
138
+ ): boolean {
139
+ if (!expr || !expr.trim()) return true;
140
+
141
+ const match = expr.match(/^(.+?)\s*(===|!==|==|!=)\s*(.+)$/);
142
+ if (!match) {
143
+ const val = resolveContextValue(expr.trim(), payload, currentBranch);
144
+ return Boolean(val);
145
+ }
146
+
147
+ const [, leftRaw, op, rightRaw] = match;
148
+ const leftVal = resolveContextValue(leftRaw.trim(), payload, currentBranch);
149
+ const rightVal = parseLiteral(rightRaw.trim());
150
+
151
+ const isEqualOp = op === "==" || op === "===";
152
+
153
+ if (
154
+ typeof rightVal === "string" &&
155
+ typeof leftVal === "string" &&
156
+ rightVal.includes("*")
157
+ ) {
158
+ const matchesGlob = globMatch(rightVal, leftVal);
159
+ return isEqualOp ? matchesGlob : !matchesGlob;
160
+ }
161
+
162
+ const areEqual = leftVal === rightVal;
163
+ return isEqualOp ? areEqual : !areEqual;
164
+ }