mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Disable GitHub workflow events by default (#528)
* Add a HookFilter class * Use the HookFilter class * Support default hooks in the web UI * Update documentation * changelog * Allow all GitLab events by default * bits of cleanup
This commit is contained in:
parent
f7bb20a639
commit
0c9bbf6410
1
changelog.d/528.feature
Normal file
1
changelog.d/528.feature
Normal file
@ -0,0 +1 @@
|
||||
Disable GitHub workflow events by default.
|
@ -27,7 +27,8 @@ This connection supports a few options which can be defined in the room state:
|
||||
|
||||
| Option | Description | Allowed values | Default |
|
||||
|--------|-------------|----------------|---------|
|
||||
|ignoreHooks|Choose to exclude notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|
||||
|enableHooks [^1]|Enable notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|
||||
|ignoreHooks [^1]|Choose to exclude notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|
||||
|commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!gh`|
|
||||
|showIssueRoomLink|When new issues are created, provide a Matrix alias link to the issue room|`true/false`|`false`|
|
||||
|prDiff|Show a diff in the room when a PR is created, subject to limits|`{enabled: boolean, maxLines: number}`|`{enabled: false}`|
|
||||
@ -40,21 +41,28 @@ This connection supports a few options which can be defined in the room state:
|
||||
|workflowRun.matchingBranch|Only report workflow runs if it matches this regex.|Regex string|*empty*|
|
||||
|
||||
|
||||
[^1]: `ignoreHooks` takes precedence over `enableHooks`.
|
||||
|
||||
|
||||
### Supported event types
|
||||
|
||||
This connection supports sending messages when the following actions happen on the repository.
|
||||
|
||||
- issue
|
||||
- issue.created
|
||||
- issue.changed
|
||||
- issue.edited
|
||||
- issue.labeled
|
||||
- pull_request
|
||||
- pull_request.closed
|
||||
- pull_request.merged
|
||||
- pull_request.opened
|
||||
- pull_request.ready_for_review
|
||||
- pull_request.reviewed
|
||||
Note: Some of these event types are enabled by default (marked with a `*`)
|
||||
|
||||
- issue *
|
||||
- issue.created *
|
||||
- issue.changed *
|
||||
- issue.edited *
|
||||
- issue.labeled *
|
||||
- pull_request *
|
||||
- pull_request.closed *
|
||||
- pull_request.merged *
|
||||
- pull_request.opened *
|
||||
- pull_request.ready_for_review *
|
||||
- pull_request.reviewed *
|
||||
- release *
|
||||
- release.created *
|
||||
- workflow.run
|
||||
- workflow.run.success
|
||||
- workflow.run.failure
|
||||
@ -63,5 +71,3 @@ This connection supports sending messages when the following actions happen on t
|
||||
- workflow.run.timed_out
|
||||
- workflow.run.stale
|
||||
- workflow.run.action_required
|
||||
- release
|
||||
- release.created
|
||||
|
@ -46,16 +46,15 @@ export class ConnectionManager extends EventEmitter {
|
||||
/**
|
||||
* Push a new connection to the manager, if this connection already
|
||||
* exists then this will no-op.
|
||||
* NOTE: The comparison only checks that the same object instance isn't present,
|
||||
* but not if two instances exist with the same type/state.
|
||||
* @param connection The connection instance to push.
|
||||
*/
|
||||
public push(...connections: IConnection[]) {
|
||||
for (const connection of connections) {
|
||||
if (!this.connections.find(c => c.connectionId === connection.connectionId)) {
|
||||
this.connections.push(connection);
|
||||
this.emit('new-connection', connection);
|
||||
if (this.connections.some(c => c.connectionId === connection.connectionId)) {
|
||||
return;
|
||||
}
|
||||
this.connections.push(connection);
|
||||
this.emit('new-connection', connection);
|
||||
}
|
||||
Metrics.connections.set(this.connections.length);
|
||||
// Already exists, noop.
|
||||
|
@ -26,6 +26,7 @@ import { ApiError, ErrCode, ValidatorApiError } from "../api";
|
||||
import { PermissionCheckFn } from ".";
|
||||
import { MinimalGitHubIssue, MinimalGitHubRepo } from "../libRs";
|
||||
import Ajv, { JSONSchemaType } from "ajv";
|
||||
import { HookFilter } from "../HookFilter";
|
||||
|
||||
const log = new Logger("GitHubRepoConnection");
|
||||
const md = new markdown();
|
||||
@ -39,6 +40,7 @@ interface IQueryRoomOpts {
|
||||
}
|
||||
|
||||
export interface GitHubRepoConnectionOptions extends IConnectionState {
|
||||
enableHooks?: AllowedEventsNames[],
|
||||
ignoreHooks?: AllowedEventsNames[],
|
||||
showIssueRoomLink?: boolean;
|
||||
prDiff?: {
|
||||
@ -126,6 +128,16 @@ const AllowedEvents: AllowedEventsNames[] = [
|
||||
"workflow.run.stale",
|
||||
];
|
||||
|
||||
/**
|
||||
* These hooks are enabled by default, unless they are
|
||||
* specifed in the ignoreHooks option.
|
||||
*/
|
||||
const AllowHookByDefault: AllowedEventsNames[] = [
|
||||
"issue",
|
||||
"pull_request",
|
||||
"release",
|
||||
];
|
||||
|
||||
const ConnectionStateSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@ -142,6 +154,13 @@ const ConnectionStateSchema = {
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
enableHooks: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
commandPrefix: {
|
||||
type: "string",
|
||||
minLength: 2,
|
||||
@ -429,6 +448,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
static helpMessage: HelpFunction;
|
||||
static botCommands: BotCommands;
|
||||
|
||||
private readonly hookFilter: HookFilter<AllowedEventsNames>;
|
||||
|
||||
public debounceOnIssueLabeled = new Map<number, {labels: Set<string>, timeout: NodeJS.Timeout}>();
|
||||
|
||||
constructor(roomId: string,
|
||||
@ -450,6 +471,11 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
"!gh",
|
||||
"github",
|
||||
);
|
||||
this.hookFilter = new HookFilter(
|
||||
AllowHookByDefault,
|
||||
state.enableHooks,
|
||||
state.ignoreHooks,
|
||||
)
|
||||
}
|
||||
|
||||
public get hotlinkIssues() {
|
||||
@ -485,6 +511,12 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
return GitHubRepoConnection.validateState(content);
|
||||
}
|
||||
|
||||
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
|
||||
await super.onStateUpdate(stateEv);
|
||||
this.hookFilter.enabledHooks = this.state.enableHooks ?? [];
|
||||
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? [];
|
||||
}
|
||||
|
||||
public isInterestedInStateEvent(eventType: string, stateKey: string) {
|
||||
return GitHubRepoConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
|
||||
}
|
||||
@ -713,7 +745,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onIssueCreated(event: IssuesOpenedEvent) {
|
||||
if (this.shouldSkipHook('issue.created', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
if (this.hookFilter.shouldSkip('issue.created', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onIssueCreated ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
||||
@ -747,7 +779,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onIssueStateChange(event: IssuesEditedEvent|IssuesReopenedEvent|IssuesClosedEvent) {
|
||||
if (this.shouldSkipHook('issue.changed', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
if (this.hookFilter.shouldSkip('issue.changed', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onIssueStateChange ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
||||
@ -793,7 +825,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onIssueEdited(event: IssuesEditedEvent) {
|
||||
if (this.shouldSkipHook('issue.edited', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
if (this.hookFilter.shouldSkip('issue.edited', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
return;
|
||||
}
|
||||
if (!event.issue) {
|
||||
@ -812,7 +844,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onIssueLabeled(event: IssuesLabeledEvent) {
|
||||
if (this.shouldSkipHook('issue.labeled', 'issue') || !event.label || !this.state.includingLabels?.length) {
|
||||
if (this.hookFilter.shouldSkip('issue.labeled', 'issue') || !event.label || !this.state.includingLabels?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -866,7 +898,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onPROpened(event: PullRequestOpenedEvent) {
|
||||
if (this.shouldSkipHook('pull_request.opened', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
if (this.hookFilter.shouldSkip('pull_request.opened', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onPROpened ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`);
|
||||
@ -902,7 +934,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onPRReadyForReview(event: PullRequestReadyForReviewEvent) {
|
||||
if (this.shouldSkipHook('pull_request.ready_for_review', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
if (this.hookFilter.shouldSkip('pull_request.ready_for_review', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onPRReadyForReview ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`);
|
||||
@ -925,7 +957,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onPRReviewed(event: PullRequestReviewSubmittedEvent) {
|
||||
if (this.shouldSkipHook('pull_request.reviewed', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
if (this.hookFilter.shouldSkip('pull_request.reviewed', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onPRReadyForReview ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`);
|
||||
@ -958,7 +990,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onPRClosed(event: PullRequestClosedEvent) {
|
||||
if (this.shouldSkipHook('pull_request.closed', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
if (this.hookFilter.shouldSkip('pull_request.closed', 'pull_request') || !this.matchesLabelFilter(event.pull_request)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onPRClosed ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`);
|
||||
@ -1004,7 +1036,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onReleaseCreated(event: ReleaseCreatedEvent) {
|
||||
if (this.shouldSkipHook('release', 'release.created')) {
|
||||
if (this.hookFilter.shouldSkip('release', 'release.created')) {
|
||||
return;
|
||||
}
|
||||
log.info(`onReleaseCreated ${this.roomId} ${this.org}/${this.repo} #${event.release.tag_name}`);
|
||||
@ -1031,7 +1063,7 @@ ${event.release.body}`;
|
||||
const workflowRunType = `workflow.run.${workflowRun.conclusion}`;
|
||||
// Type safety checked above.
|
||||
if (
|
||||
this.shouldSkipHook('workflow', 'workflow.run', workflowRunType as AllowedEventsNames)) {
|
||||
this.hookFilter.shouldSkip('workflow', 'workflow.run', workflowRunType as AllowedEventsNames)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1101,17 +1133,6 @@ ${event.release.body}`;
|
||||
return `GitHubRepo ${this.org}/${this.repo}`;
|
||||
}
|
||||
|
||||
private shouldSkipHook(...hookName: AllowedEventsNames[]) {
|
||||
if (this.state.ignoreHooks) {
|
||||
for (const name of hookName) {
|
||||
if (this.state.ignoreHooks?.includes(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static getProvisionerDetails(botUserId: string) {
|
||||
return {
|
||||
service: "github",
|
||||
@ -1203,6 +1224,8 @@ ${event.release.body}`;
|
||||
const validatedConfig = GitHubRepoConnection.validateState(config);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, this.stateKey, validatedConfig);
|
||||
this.state = validatedConfig;
|
||||
this.hookFilter.enabledHooks = this.state.enableHooks ?? [];
|
||||
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? [];
|
||||
}
|
||||
|
||||
public async onRemove() {
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
|
||||
import { MatrixMessageContent } from "../MatrixEvent";
|
||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
import markdown from "markdown-it";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
|
||||
@ -15,6 +15,7 @@ import { ErrCode, ApiError, ValidatorApiError } from "../api"
|
||||
import { AccessLevel } from "../Gitlab/Types";
|
||||
import Ajv, { JSONSchemaType } from "ajv";
|
||||
import { CommandError } from "../errors";
|
||||
import { HookFilter } from "../HookFilter";
|
||||
|
||||
export interface GitLabRepoConnectionState extends IConnectionState {
|
||||
instance: string;
|
||||
@ -287,6 +288,8 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
approved?: boolean,
|
||||
}>();
|
||||
|
||||
private readonly hookFilter: HookFilter<AllowedEventsNames>;
|
||||
|
||||
constructor(roomId: string,
|
||||
stateKey: string,
|
||||
private readonly as: Appservice,
|
||||
@ -307,6 +310,12 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
if (!state.path || !state.instance) {
|
||||
throw Error('Invalid state, missing `path` or `instance`');
|
||||
}
|
||||
this.hookFilter = new HookFilter(
|
||||
// GitLab allows all events by default
|
||||
AllowedEvents,
|
||||
[],
|
||||
state.ignoreHooks,
|
||||
);
|
||||
}
|
||||
|
||||
public get path() {
|
||||
@ -325,6 +334,11 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
return GitLabRepoConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
|
||||
}
|
||||
|
||||
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
|
||||
await super.onStateUpdate(stateEv);
|
||||
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? [];
|
||||
}
|
||||
|
||||
public getProvisionerDetails(): GitLabRepoResponseItem {
|
||||
return {
|
||||
...GitLabRepoConnection.getProvisionerDetails(this.as.botUserId),
|
||||
@ -403,7 +417,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
}
|
||||
|
||||
public async onMergeRequestOpened(event: IGitLabWebhookMREvent) {
|
||||
if (this.shouldSkipHook('merge_request', 'merge_request.open') || !this.matchesLabelFilter(event)) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.open') || !this.matchesLabelFilter(event)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onMergeRequestOpened ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
||||
@ -419,7 +433,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
}
|
||||
|
||||
public async onMergeRequestClosed(event: IGitLabWebhookMREvent) {
|
||||
if (this.shouldSkipHook('merge_request', 'merge_request.close') || !this.matchesLabelFilter(event)) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.close') || !this.matchesLabelFilter(event)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onMergeRequestClosed ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
||||
@ -435,7 +449,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
}
|
||||
|
||||
public async onMergeRequestMerged(event: IGitLabWebhookMREvent) {
|
||||
if (this.shouldSkipHook('merge_request', 'merge_request.merge') || !this.matchesLabelFilter(event)) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.merge') || !this.matchesLabelFilter(event)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onMergeRequestMerged ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
||||
@ -451,7 +465,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
}
|
||||
|
||||
public async onMergeRequestUpdate(event: IGitLabWebhookMREvent) {
|
||||
if (this.shouldSkipHook('merge_request', 'merge_request.ready_for_review')) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.ready_for_review')) {
|
||||
return;
|
||||
}
|
||||
log.info(`onMergeRequestUpdate ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
||||
@ -483,7 +497,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
}
|
||||
|
||||
public async onGitLabTagPush(event: IGitLabWebhookTagPushEvent) {
|
||||
if (this.shouldSkipHook('tag_push')) {
|
||||
if (this.hookFilter.shouldSkip('tag_push')) {
|
||||
return;
|
||||
}
|
||||
log.info(`onGitLabTagPush ${this.roomId} ${this.instance.url}/${this.path} ${event.ref}`);
|
||||
@ -503,7 +517,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
|
||||
|
||||
public async onGitLabPush(event: IGitLabWebhookPushEvent) {
|
||||
if (this.shouldSkipHook('push')) {
|
||||
if (this.hookFilter.shouldSkip('push')) {
|
||||
return;
|
||||
}
|
||||
log.info(`onGitLabPush ${this.roomId} ${this.instance.url}/${this.path} ${event.after}`);
|
||||
@ -542,7 +556,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
|
||||
public async onWikiPageEvent(data: IGitLabWebhookWikiPageEvent) {
|
||||
const attributes = data.object_attributes;
|
||||
if (this.shouldSkipHook('wiki', `wiki.${attributes.action}`)) {
|
||||
if (this.hookFilter.shouldSkip('wiki', `wiki.${attributes.action}`)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onWikiPageEvent ${this.roomId} ${this.instance}/${this.path}`);
|
||||
@ -568,7 +582,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
}
|
||||
|
||||
public async onRelease(data: IGitLabWebhookReleaseEvent) {
|
||||
if (this.shouldSkipHook('release', 'release.created')) {
|
||||
if (this.hookFilter.shouldSkip('release', 'release.created')) {
|
||||
return;
|
||||
}
|
||||
log.info(`onReleaseCreated ${this.roomId} ${this.toString()} ${data.tag}`);
|
||||
@ -656,7 +670,7 @@ ${data.description}`;
|
||||
}
|
||||
|
||||
public async onMergeRequestReviewed(event: IGitLabWebhookMREvent) {
|
||||
if (this.shouldSkipHook('merge_request', 'merge_request.review', `merge_request.${event.object_attributes.action}`) || !this.matchesLabelFilter(event)) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.review', `merge_request.${event.object_attributes.action}`) || !this.matchesLabelFilter(event)) {
|
||||
return;
|
||||
}
|
||||
log.info(`onMergeRequestReviewed ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
||||
@ -677,7 +691,7 @@ ${data.description}`;
|
||||
}
|
||||
|
||||
public async onCommentCreated(event: IGitLabWebhookNoteEvent) {
|
||||
if (this.shouldSkipHook('merge_request', 'merge_request.review', 'merge_request.review.comments')) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.review', 'merge_request.review.comments')) {
|
||||
return;
|
||||
}
|
||||
log.info(`onCommentCreated ${this.roomId} ${this.toString()} ${event.merge_request?.iid} ${event.object_attributes.id}`);
|
||||
@ -708,24 +722,13 @@ ${data.description}`;
|
||||
return true;
|
||||
}
|
||||
|
||||
private shouldSkipHook(...hookName: AllowedEventsNames[]) {
|
||||
if (this.state.ignoreHooks) {
|
||||
for (const name of hookName) {
|
||||
if (this.state.ignoreHooks?.includes(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async provisionerUpdateConfig(userId: string, config: Record<string, unknown>) {
|
||||
const validatedConfig = GitLabRepoConnection.validateState(config);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitLabRepoConnection.CanonicalEventType, this.stateKey, validatedConfig);
|
||||
this.state = validatedConfig;
|
||||
this.hookFilter.ignoredHooks = this.state.ignoreHooks ?? [];
|
||||
}
|
||||
|
||||
|
||||
public async onRemove() {
|
||||
log.info(`Removing ${this.toString()} for ${this.roomId}`);
|
||||
// Do a sanity check that the event exists.
|
||||
|
19
src/HookFilter.ts
Normal file
19
src/HookFilter.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export class HookFilter<T extends string> {
|
||||
constructor(
|
||||
public readonly defaultHooks: T[],
|
||||
public enabledHooks: T[] = [],
|
||||
public ignoredHooks: T[] = []
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public shouldSkip(...hookName: T[]) {
|
||||
if (hookName.some(name => this.ignoredHooks.includes(name))) {
|
||||
return true;
|
||||
}
|
||||
if (hookName.some(name => this.enabledHooks.includes(name))) {
|
||||
return false;
|
||||
}
|
||||
return !hookName.some(h => this.defaultHooks.includes(h));
|
||||
}
|
||||
}
|
31
tests/HookFilter.ts
Normal file
31
tests/HookFilter.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { expect } from "chai";
|
||||
import { HookFilter } from '../src/HookFilter';
|
||||
|
||||
const DEFAULT_SET = ['default-allowed', 'default-allowed-but-ignored'];
|
||||
const ENABLED_SET = ['enabled-hook', 'enabled-but-ignored'];
|
||||
const IGNORED_SET = ['ignored', 'enabled-but-ignored', 'default-allowed-but-ignored'];
|
||||
|
||||
describe("HookFilter", () => {
|
||||
let filter: HookFilter<string>;
|
||||
beforeEach(() => {
|
||||
filter = new HookFilter(DEFAULT_SET, ENABLED_SET, IGNORED_SET);
|
||||
});
|
||||
it('should skip a hook named in ignoreHooks', () => {
|
||||
expect(filter.shouldSkip('ignored')).to.be.true;
|
||||
});
|
||||
it('should allow a hook named in defaults', () => {
|
||||
expect(filter.shouldSkip('default-allowed')).to.be.false;
|
||||
});
|
||||
it('should allow a hook named in enabled', () => {
|
||||
expect(filter.shouldSkip('enabled-hook')).to.be.false;
|
||||
});
|
||||
it('should skip a hook named in defaults but also in ignored', () => {
|
||||
expect(filter.shouldSkip('default-allowed-but-ignored')).to.be.true;
|
||||
});
|
||||
it('should skip a hook named in enabled but also in ignored', () => {
|
||||
expect(filter.shouldSkip('enabled-but-ignored')).to.be.true;
|
||||
});
|
||||
it('should skip if any hooks are in ignored', () => {
|
||||
expect(filter.shouldSkip('enabled-hook', 'enabled-but-ignored')).to.be.true;
|
||||
});
|
||||
});
|
@ -2,7 +2,7 @@ import { h, FunctionComponent } from "preact";
|
||||
import style from "./InputField.module.scss";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
label?: string;
|
||||
noPadding: boolean;
|
||||
|
@ -120,18 +120,41 @@ const ConnectionSearch: FunctionComponent<{api: BridgeAPI, onPicked: (state: Git
|
||||
}
|
||||
|
||||
const EventCheckbox: FunctionComponent<{
|
||||
ignoredHooks: string[],
|
||||
ignoredHooks?: string[],
|
||||
enabledHooks?: string[],
|
||||
onChange: (evt: HTMLInputElement) => void,
|
||||
eventName: string,
|
||||
parentEvent?: string,
|
||||
}> = ({ignoredHooks, onChange, eventName, parentEvent, children}) => {
|
||||
}> = ({ignoredHooks, enabledHooks, onChange, eventName, parentEvent, children}) => {
|
||||
let disabled = false;
|
||||
let checked = false;
|
||||
|
||||
if (!enabledHooks && !ignoredHooks) {
|
||||
throw Error(`Invalid configuration for checkbox ${eventName}`);
|
||||
}
|
||||
|
||||
if (enabledHooks) {
|
||||
disabled = !!(parentEvent && !enabledHooks.includes(parentEvent));
|
||||
checked = enabledHooks.includes(eventName);
|
||||
if (ignoredHooks?.includes(eventName)) {
|
||||
// If both are set, this was previously a on-by-default event
|
||||
// that is now off-by-default, and so we need to check both fields.
|
||||
disabled = !!(parentEvent && ignoredHooks.includes(parentEvent));
|
||||
checked = true
|
||||
}
|
||||
} else if (ignoredHooks) {
|
||||
// If enabled hooks is not set, this is on-by-default hook.
|
||||
disabled = !!(parentEvent && ignoredHooks.includes(parentEvent));
|
||||
checked = !ignoredHooks.includes(eventName);
|
||||
}
|
||||
|
||||
return <li>
|
||||
<label>
|
||||
<input
|
||||
disabled={parentEvent && ignoredHooks.includes(parentEvent)}
|
||||
disabled={disabled}
|
||||
type="checkbox"
|
||||
x-event-name={eventName}
|
||||
checked={!ignoredHooks.includes(eventName)}
|
||||
checked={checked}
|
||||
onChange={onChange} />
|
||||
{ children }
|
||||
</label>
|
||||
@ -140,6 +163,8 @@ const EventCheckbox: FunctionComponent<{
|
||||
|
||||
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitHubRepoResponseItem, GitHubRepoConnectionState>> = ({api, existingConnection, onSave, onRemove }) => {
|
||||
const [ignoredHooks, setIgnoredHooks] = useState<string[]>(existingConnection?.config.ignoreHooks || []);
|
||||
// Only used for off-by-default hooks.
|
||||
const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []);
|
||||
|
||||
const toggleIgnoredHook = useCallback(evt => {
|
||||
const key = (evt.target as HTMLElement).getAttribute('x-event-name');
|
||||
@ -147,8 +172,26 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
|
||||
setIgnoredHooks(ignoredHooks => (
|
||||
ignoredHooks.includes(key) ? ignoredHooks.filter(k => k !== key) : [...ignoredHooks, key]
|
||||
));
|
||||
// Remove from enabledHooks
|
||||
setEnabledHooks(enabledHooks => (
|
||||
enabledHooks.filter(k => k !== key)
|
||||
));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleEnabledHook = useCallback(evt => {
|
||||
const key = (evt.target as HTMLElement).getAttribute('x-event-name');
|
||||
if (key) {
|
||||
setEnabledHooks(enabledHooks => (
|
||||
enabledHooks.includes(key) ? enabledHooks.filter(k => k !== key) : [...enabledHooks, key]
|
||||
));
|
||||
// Remove from ignoreHooks
|
||||
setIgnoredHooks(ignoredHooks => (
|
||||
ignoredHooks.filter(k => k !== key)
|
||||
));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [connectionState, setConnectionState] = useState<GitHubRepoConnectionState|null>(null);
|
||||
|
||||
const canEdit = !existingConnection || (existingConnection?.canEdit ?? false);
|
||||
@ -163,10 +206,11 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
|
||||
onSave({
|
||||
...(state),
|
||||
ignoreHooks: ignoredHooks as any[],
|
||||
enableHooks: enabledHooks as any[],
|
||||
commandPrefix: commandPrefixRef.current?.value || commandPrefixRef.current?.placeholder,
|
||||
});
|
||||
}
|
||||
}, [canEdit, existingConnection, connectionState, ignoredHooks, commandPrefixRef, onSave]);
|
||||
}, [enabledHooks, canEdit, existingConnection, connectionState, ignoredHooks, commandPrefixRef, onSave]);
|
||||
|
||||
return <form onSubmit={handleSave}>
|
||||
{!existingConnection && <ConnectionSearch api={api} onPicked={setConnectionState} />}
|
||||
@ -191,15 +235,15 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.ready_for_review" onChange={toggleIgnoredHook}>Ready for review</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="pull_request" eventName="pull_request.reviewed" onChange={toggleIgnoredHook}>Reviewed</EventCheckbox>
|
||||
</ul>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} eventName="workflow.run" onChange={toggleIgnoredHook}>Workflow Runs</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} eventName="workflow.run" onChange={toggleEnabledHook}>Workflow Runs</EventCheckbox>
|
||||
<ul>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.success" onChange={toggleIgnoredHook}>Success</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.failure" onChange={toggleIgnoredHook}>Failed</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.neutral" onChange={toggleIgnoredHook}>Neutral</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.cancelled" onChange={toggleIgnoredHook}>Cancelled</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.timed_out" onChange={toggleIgnoredHook}>Timed Out</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.action_required" onChange={toggleIgnoredHook}>Action Required</EventCheckbox>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.stale" onChange={toggleIgnoredHook}>Stale</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.success" onChange={toggleEnabledHook}>Success</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.failure" onChange={toggleEnabledHook}>Failed</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.neutral" onChange={toggleEnabledHook}>Neutral</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.cancelled" onChange={toggleEnabledHook}>Cancelled</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.timed_out" onChange={toggleEnabledHook}>Timed Out</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.action_required" onChange={toggleEnabledHook}>Action Required</EventCheckbox>
|
||||
<EventCheckbox enabledHooks={enabledHooks} ignoredHooks={ignoredHooks} parentEvent="workflow.run" eventName="workflow.run.stale" onChange={toggleEnabledHook}>Stale</EventCheckbox>
|
||||
</ul>
|
||||
<EventCheckbox ignoredHooks={ignoredHooks} eventName="release" onChange={toggleIgnoredHook}>Releases</EventCheckbox>
|
||||
</ul>
|
||||
|
Loading…
x
Reference in New Issue
Block a user