-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: config event handler #18
Changes from 1 commit
89f7de7
b17af14
10887c3
084d1c1
72fdeef
441ebbd
7c6043a
5f0e142
ea1b1d7
0979fd7
3c0b829
b224246
295284a
b02d104
7bd5ff8
54c29c8
b5fd06d
d769841
92e48bf
0d83199
387d33b
597d9ce
37cb25c
c000369
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,63 @@ | ||
import { StaticDecode, Type } from "@sinclair/typebox"; | ||
import { GitHubContext } from "../github-context"; | ||
import { dispatchWorkflow, getDefaultBranch } from "../utils/workflow-dispatch"; | ||
import { Value } from "@sinclair/typebox/value"; | ||
|
||
export async function repositoryDispatch(event: GitHubContext<"repository_dispatch">) { | ||
console.log("Repository dispatch event received", event.payload); | ||
export async function repositoryDispatch(context: GitHubContext<"repository_dispatch">) { | ||
console.log("Repository dispatch event received", context.payload.client_payload); | ||
|
||
const pluginOutput = context.payload.client_payload as PluginOutput; | ||
|
||
if (!Value.Check(pluginOutputSchema, pluginOutput)) { | ||
const errors = [...Value.Errors(pluginOutputSchema, pluginOutput)]; | ||
console.error("Invalid environment variables", errors); | ||
throw new Error("Invalid environment variables"); | ||
} | ||
|
||
const state = await context.eventHandler.pluginChainState.get(pluginOutput.id); | ||
if (!state) { | ||
console.error("No state found for plugin chain"); | ||
return; | ||
} | ||
|
||
const currentPlugin = state.pluginChain[state.currentPlugin]; | ||
if (currentPlugin.plugin.owner !== context.payload.repository.owner.login || currentPlugin.plugin.repo !== context.payload.repository.name) { | ||
console.error("Plugin chain state does not match payload"); | ||
return; | ||
} | ||
|
||
state.currentPlugin++; | ||
await context.eventHandler.pluginChainState.put(pluginOutput.id, state); | ||
|
||
const nextPlugin = state.pluginChain[state.currentPlugin]; | ||
if (!nextPlugin) { | ||
console.log("No more plugins to call"); | ||
return; | ||
} | ||
|
||
console.log("Dispatching next plugin", nextPlugin); | ||
|
||
const inputs = { | ||
...pluginOutput.output, | ||
id: pluginOutput.id, | ||
settings: JSON.stringify(nextPlugin.with), | ||
ref: nextPlugin.plugin.ref ?? (await getDefaultBranch(context, nextPlugin.plugin.owner, nextPlugin.plugin.repo)), | ||
}; | ||
|
||
await dispatchWorkflow(context, { | ||
owner: nextPlugin.plugin.owner, | ||
repository: nextPlugin.plugin.repo, | ||
ref: nextPlugin.plugin.ref, | ||
workflowId: nextPlugin.plugin.workflowId, | ||
inputs: inputs, | ||
}); | ||
} | ||
|
||
const pluginOutputSchema = Type.Object({ | ||
id: Type.String(), | ||
owner: Type.String(), | ||
repo: Type.String(), | ||
output: Type.Any(), | ||
}); | ||
|
||
type PluginOutput = StaticDecode<typeof pluginOutputSchema>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,32 +2,58 @@ import { Type as T } from "@sinclair/typebox"; | |
import { StaticDecode } from "@sinclair/typebox"; | ||
import { githubWebhookEvents } from "./webhook-events"; | ||
|
||
enum Commands { | ||
Start = "start", | ||
Stop = "stop", | ||
const pluginNameRegex = new RegExp("^([0-9a-zA-Z-._]+)/([0-9a-zA-Z-._]+)(?::([0-9a-zA-Z-._]+))?(?:@([0-9a-zA-Z]+))?$"); | ||
|
||
type GithubPlugin = { | ||
owner: string; | ||
repo: string; | ||
workflowId: string; | ||
ref?: string; | ||
}; | ||
|
||
function githubPluginType() { | ||
return T.Transform(T.String()) | ||
.Decode((value) => { | ||
const matches = value.match(pluginNameRegex); | ||
if (!matches) { | ||
throw new Error(`Invalid plugin name: ${value}`); | ||
} | ||
return { | ||
owner: matches[1], | ||
repo: matches[2], | ||
workflowId: matches[3] || "compute.yml", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will default to This is to conform to the existing approach that GitHub Actions takes, and to enable compatibility with all existing GitHub Actions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you can invoke workflows in the root directory because those are meant to be published to the marketplace and used in a workflow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can just make it a pass through action. Basically the only logic will be to invoke a workflow in the workflow folder. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think we should default to |
||
ref: matches[4] || undefined, | ||
} as GithubPlugin; | ||
}) | ||
.Encode((value) => { | ||
return `${value.owner}/${value.repo}${value.workflowId ? ":" + value.workflowId : ""}${value.ref ? "@" + value.ref : ""}`; | ||
}); | ||
} | ||
|
||
const pluginChainSchema = T.Array( | ||
T.Object({ | ||
plugin: githubPluginType(), | ||
type: T.Union([T.Literal("github")], { default: "github" }), | ||
with: T.Optional(T.Unknown()), | ||
}), | ||
{ minItems: 1 } | ||
); | ||
|
||
export type PluginChain = StaticDecode<typeof pluginChainSchema>; | ||
|
||
const handlerSchema = T.Array( | ||
T.Object({ | ||
workflow: T.Object({ | ||
owner: T.String(), | ||
repository: T.String(), | ||
workflowId: T.String(), | ||
ref: T.Optional(T.String()), | ||
}), | ||
settings: T.Optional(T.Unknown()), | ||
name: T.Optional(T.String()), | ||
description: T.Optional(T.String()), | ||
command: T.Optional(T.String()), | ||
example: T.Optional(T.String()), | ||
uses: pluginChainSchema, | ||
}), | ||
{ default: [] } | ||
); | ||
|
||
export const configSchema = T.Object({ | ||
handlers: T.Object( | ||
{ | ||
commands: T.Record(T.Enum(Commands), handlerSchema, { default: {} }), | ||
events: T.Record(T.Enum(githubWebhookEvents), handlerSchema, { default: {} }), | ||
}, | ||
{ default: {} } | ||
), | ||
plugins: T.Record(T.Enum(githubWebhookEvents), handlerSchema, { default: {} }), | ||
}); | ||
|
||
export type Config = StaticDecode<typeof configSchema>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import { Type as T, type Static } from "@sinclair/typebox"; | ||
|
||
export const envSchema = T.Object({ WEBHOOK_SECRET: T.String(), APP_ID: T.String(), PRIVATE_KEY: T.String() }); | ||
export type Env = Static<typeof envSchema>; | ||
|
||
export type Env = Static<typeof envSchema> & { | ||
PLUGIN_CHAIN_STATE: KVNamespace; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { PluginChain } from "./config"; | ||
|
||
export type PluginChainState = { | ||
currentPlugin: number; | ||
pluginChain: PluginChain; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export class CloudflareKV<T> { | ||
private _kv: KVNamespace; | ||
|
||
constructor(kv: KVNamespace) { | ||
this._kv = kv; | ||
} | ||
|
||
get(id: string): Promise<T | null> { | ||
return this._kv.get(id, "json"); | ||
} | ||
|
||
put(id: string, state: T): Promise<void> { | ||
return this._kv.put(id, JSON.stringify(state)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,11 @@ name = "ubiquibot-worker" | |
main = "src/worker.ts" | ||
compatibility_date = "2023-12-06" | ||
|
||
[env.dev] | ||
kv_namespaces = [ | ||
{ binding = "PLUGIN_CHAIN_STATE", id = "a50ef0a143e449c087b6be613a84e5ba" }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is my expectation that we will handle all storage from within plugins. What is the kernel seeking to remember? Can you elaborate on your decision here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For one event type there can be multiple plugin chains, so when the plugins return data to the kernel, the kernel needs to know which plugin chain is running and which plugin in the chain is currently executing. issues_label.added:
- name: "Help Menu"
description: "This will parse the config and display the help menu for slash commands."
command: "^\/help$"
example: "/help"
uses:
- ubiquibot/plugin-A
- ubiquibot/plugin-B
- name: "Assistive Pricing"
description: "Set pricing based on Time and Priority labels."
uses:
- ubiquibot/plugin-B
- ubiquibot/plugin-C
- ubiquibot/plugin-A Another reason is because the config can change when the plugin is running so we need to save the plugin chain at the invocation of the first plugin in the chain. It also has a nice side feature of preventing plugins from sending data to the kernel without any invocation from the kernel |
||
] | ||
|
||
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) | ||
# Note: Use secrets to store sensitive data. | ||
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the kernel need repository dispatch event handler for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that plugins will use repository dispatch to return data to the kernel, which will call the next plugin in the chain or stop if it's the last one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. If that approach works then its great! I'm not sure because I never ran the stable kernel, so I never tested any plugin interfacing.