Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"dev:link": "deco link -p 3004 -- PORT=3004 bun run dev"
},
"dependencies": {
"@decocms/bindings": "1.3.4",
"@decocms/runtime": "1.2.13",
"@decocms/bindings": "^1.4.0",
"@decocms/runtime": "^1.3.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"zod": "^4.0.0"
},
Expand Down
213 changes: 103 additions & 110 deletions github/server/lib/trigger-store.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,105 @@
/**
* Trigger Store
*
* In-memory trigger configuration storage and static trigger definitions
* for the Mesh automations system.
*/
import { createTriggers } from "@decocms/runtime/triggers";
import { StudioKV } from "@decocms/runtime/trigger-storage";
import { z } from "zod";

import type { TriggerDefinition } from "@decocms/bindings/trigger";
const storage =
process.env.MESH_URL && process.env.MESH_API_KEY
? new StudioKV({
url: process.env.MESH_URL,
apiKey: process.env.MESH_API_KEY,
})
: undefined;

interface TriggerConfig {
type: string;
params: Record<string, string>;
enabled: boolean;
connectionId: string;
}

const REPO_PARAM_SCHEMA: TriggerDefinition["paramsSchema"] = {
repo: { type: "string", description: "Repository full name e.g. owner/repo" },
};

export const GITHUB_TRIGGER_DEFINITIONS = [
{
type: "github.push",
description: "Code pushed to a branch",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.pull_request.opened",
description: "Pull request opened",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.pull_request.closed",
description: "Pull request closed or merged",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.pull_request.review_requested",
description: "Review requested on a PR",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.issues.opened",
description: "Issue opened",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.issues.closed",
description: "Issue closed",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.issue_comment.created",
description: "Comment on issue or PR",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.pull_request_review.submitted",
description: "PR review submitted",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.release.published",
description: "Release published",
paramsSchema: REPO_PARAM_SCHEMA,
},
{
type: "github.workflow_run.completed",
description: "Actions workflow completed",
paramsSchema: REPO_PARAM_SCHEMA,
},
] satisfies TriggerDefinition[];

const triggerConfigs = new Map<string, TriggerConfig>();

const KNOWN_TYPES = new Set(GITHUB_TRIGGER_DEFINITIONS.map((d) => d.type));

export function listTriggerDefinitions(): TriggerDefinition[] {
return GITHUB_TRIGGER_DEFINITIONS;
}

export function configureTrigger(
type: string,
params: Record<string, string>,
enabled: boolean,
connectionId: string,
): void {
if (!KNOWN_TYPES.has(type)) {
throw new Error(`Unknown trigger type: ${type}`);
}
const key = `${connectionId}::${type}::${params.repo ?? "*"}`;
if (enabled) {
triggerConfigs.set(key, { type, params, enabled, connectionId });
} else {
triggerConfigs.delete(key);
}
}

export function hasMatchingTrigger(
eventType: string,
repo: string | undefined,
connectionId: string,
): boolean {
for (const config of triggerConfigs.values()) {
if (!config.enabled) continue;
if (config.connectionId !== connectionId) continue;
if (config.type !== eventType) continue;
if (config.params.repo && repo && config.params.repo !== repo) continue;
return true;
}
return false;
}
export const triggers = createTriggers({
definitions: [
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The trigger definitions list drops the previously supported "github.pull_request.review_requested" and "github.release.published" events. Existing automations for those events will stop being configurable or matched after this change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At github/server/lib/trigger-store.ts, line 14:

<comment>The trigger definitions list drops the previously supported "github.pull_request.review_requested" and "github.release.published" events. Existing automations for those events will stop being configurable or matched after this change.</comment>

<file context>
@@ -1,138 +1,106 @@
-  return callbackCredentials.get(connectionId);
-}
+export const triggers = createTriggers({
+  definitions: [
+    {
+      type: "github.push",
</file context>
Fix with Cubic

{
type: "github.push",
description: "Triggered when code is pushed to a repository",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.pull_request.opened",
description: "Triggered when a pull request is opened",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.pull_request.closed",
description: "Triggered when a pull request is closed or merged",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.issues.opened",
description: "Triggered when an issue is opened",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.issues.closed",
description: "Triggered when an issue is closed",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.issue_comment.created",
description: "Triggered when a comment is added to an issue or PR",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.pull_request_review.submitted",
description: "Triggered when a pull request review is submitted",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
{
type: "github.workflow_run.completed",
description: "Triggered when a GitHub Actions workflow run completes",
params: z.object({
repo: z
.string()
.describe(
"Repository full name (owner/repo). Leave empty for all repos.",
),
}),
},
],
storage,
});
57 changes: 3 additions & 54 deletions github/server/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,10 @@
* GitHub MCP Tools
*
* All tools come from the upstream MCP server via the proxy,
* plus trigger tools for the Mesh automations system.
* plus trigger tools from the @decocms/runtime triggers SDK.
*/

import { createTool } from "@decocms/runtime/tools";
import {
TriggerListInputSchema,
TriggerListOutputSchema,
TriggerConfigureInputSchema,
TriggerConfigureOutputSchema,
} from "@decocms/bindings/trigger";
import type { Env } from "../types/env.ts";
import { createUpstreamToolsProvider } from "../lib/mcp-proxy.ts";
import {
listTriggerDefinitions,
configureTrigger,
} from "../lib/trigger-store.ts";
import { triggers } from "../lib/trigger-store.ts";

const createTriggerListTool = (_env: Env) =>
createTool({
id: "TRIGGER_LIST",
description:
"List available GitHub event triggers that can be configured for automations",
inputSchema: TriggerListInputSchema,
outputSchema: TriggerListOutputSchema,
execute: async () => {
return { triggers: listTriggerDefinitions() };
},
});

const createTriggerConfigureTool = (_env: Env) =>
createTool({
id: "TRIGGER_CONFIGURE",
description: "Enable or disable a GitHub event trigger for automations",
inputSchema: TriggerConfigureInputSchema.extend({
params: TriggerConfigureInputSchema.shape.params.default({}),
}),
outputSchema: TriggerConfigureOutputSchema,
execute: async ({ context, runtimeContext }) => {
const connectionId = (runtimeContext?.env as unknown as Env)
?.MESH_REQUEST_CONTEXT?.connectionId;
if (!connectionId) {
throw new Error("Connection ID not available");
}
configureTrigger(
context.type,
context.params,
context.enabled,
connectionId,
);
return { success: true };
},
});

export const tools = [
createUpstreamToolsProvider(),
createTriggerListTool,
createTriggerConfigureTool,
];
export const tools = [createUpstreamToolsProvider(), () => triggers.tools()];
30 changes: 13 additions & 17 deletions github/server/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { getConnectionForInstallation } from "./lib/installation-map.ts";
import { verifyGitHubWebhook } from "./lib/webhook.ts";
import { hasMatchingTrigger } from "./lib/trigger-store.ts";
import { triggers } from "./lib/trigger-store.ts";

const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || "";

Expand All @@ -27,13 +27,11 @@ export async function handleGitHubWebhook(req: Request): Promise<Response> {

const installationId = payload.installation?.id;
if (!installationId) {
console.log("[Webhook] No installation.id in payload, skipping");
return Response.json({ ok: true, skipped: "no_installation_id" });
}

const connectionId = getConnectionForInstallation(installationId);
if (!connectionId) {
console.log(`[Webhook] No mapping for installation ${installationId}`);
return Response.json({ ok: true, skipped: "no_mapping" });
}

Expand All @@ -45,21 +43,19 @@ export async function handleGitHubWebhook(req: Request): Promise<Response> {
const subject =
payload.repository?.full_name || payload.organization?.login || "unknown";

console.log(
`[Webhook] ${fullEventType} | subject=${subject} | connection=${connectionId} | sender=${payload.sender?.login}`,
// Notify Mesh — the SDK handles credential lookup and delivery
triggers.notify(
connectionId,
fullEventType as Parameters<typeof triggers.notify>[1],
{
event: fullEventType,
subject,
sender: payload.sender?.login,
repository: payload.repository?.full_name,
action: payload.action,
payload,
},
);

if (
hasMatchingTrigger(
fullEventType,
payload.repository?.full_name,
connectionId,
)
) {
console.log(
`[Webhook] TRIGGER MATCHED: ${fullEventType} | connection=${connectionId} | installation=${installationId}`,
);
}

return Response.json({ ok: true, event: fullEventType, subject });
}
Loading