mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Add setup command for GitLab (#321)
* Cleanup gitlab auth command * Add support for the `!hookshot gitlab project` command * Improve docs * changelog * add new page * Wording Co-authored-by: Tadeusz Sośnierz <tadzik@tadzik.net> * Remove useless comment Co-authored-by: Tadeusz Sośnierz <tadzik@tadzik.net>
This commit is contained in:
parent
4463e1c112
commit
f56545bf7c
1
changelog.d/321.feature
Normal file
1
changelog.d/321.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add new `!hookshot gitlab project` command to configure project bridges in rooms. See [the docs](https://matrix-org.github.io/matrix-hookshot/latest/usage/room_configuration/gitlab_project.html) for instructions.
|
@ -44,6 +44,7 @@ gitlab:
|
|||||||
url: https://gitlab.com
|
url: https://gitlab.com
|
||||||
webhook:
|
webhook:
|
||||||
secret: secrettoken
|
secret: secrettoken
|
||||||
|
publicUrl: https://example.com/hookshot/
|
||||||
userIdPrefix:
|
userIdPrefix:
|
||||||
# (Optional) Prefix used when creating ghost users for GitLab accounts.
|
# (Optional) Prefix used when creating ghost users for GitLab accounts.
|
||||||
#
|
#
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
- [Authenticating](./usage/auth.md)
|
- [Authenticating](./usage/auth.md)
|
||||||
- [Room Configuration](./usage/room_configuration.md)
|
- [Room Configuration](./usage/room_configuration.md)
|
||||||
- [GitHub Repo](./usage/room_configuration/github_repo.md)
|
- [GitHub Repo](./usage/room_configuration/github_repo.md)
|
||||||
|
- [GitLab Project](./usage/room_configuration/gitlab_project.md)
|
||||||
- [📊 Metrics](./metrics.md)
|
- [📊 Metrics](./metrics.md)
|
||||||
|
|
||||||
# 🥼 Advanced
|
# 🥼 Advanced
|
||||||
|
@ -12,6 +12,7 @@ GitLab configuration is fairly straight-forward:
|
|||||||
url: https://gitlab.com
|
url: https://gitlab.com
|
||||||
webhook:
|
webhook:
|
||||||
secret: secrettoken
|
secret: secrettoken
|
||||||
|
publicUrl: https://example.com/webhooks/
|
||||||
```
|
```
|
||||||
|
|
||||||
You need to list all the instances you plan to connect to in the `config.yml`. This is
|
You need to list all the instances you plan to connect to in the `config.yml`. This is
|
||||||
@ -21,31 +22,10 @@ to specify an instance.
|
|||||||
You should generate a webhook `secret` (e.g. `pwgen -n 64 -s 1`) and then use this as your
|
You should generate a webhook `secret` (e.g. `pwgen -n 64 -s 1`) and then use this as your
|
||||||
"Secret token" when adding webhooks.
|
"Secret token" when adding webhooks.
|
||||||
|
|
||||||
|
The `publicUrl` must be the URL where GitLab webhook events are received (i.e. the path to `/`
|
||||||
|
for your `webhooks` listener).
|
||||||
|
|
||||||
## Adding a repository
|
## Adding a repository
|
||||||
|
|
||||||
Adding a repository is a case of navigating to the settings page, and then adding a new webhook.
|
You can now follow the guide on [authenticating with GitLab](../usage/auth.md), and then [bridging a room](../usage/room_configuration/gitlab_project.md#setting-up)
|
||||||
You will want to give the URL of the public address for the hookshot webhooks port on the `/` path.
|
|
||||||
|
|
||||||
You should use the value of `webhook.secret` from your config as your "Secret token".
|
|
||||||
|
|
||||||
You should add the events you wish to trigger on. Hookshot currently supports:
|
|
||||||
|
|
||||||
- Push events
|
|
||||||
- Tag events
|
|
||||||
- Issues events
|
|
||||||
- Merge request events
|
|
||||||
- Releases events
|
|
||||||
|
|
||||||
You will need to do this each time you want to a repository to hookshot.
|
|
||||||
|
|
||||||
To then bridge a room to GitLab, you will need to add a `uk.half-shot.matrix-hookshot.gitlab.repository`
|
|
||||||
*state event* to a room containing a content of:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
"instance": "gitlab", // your instance name
|
|
||||||
"path": "yourusername/repo" // the full path to the repo
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Once this is done, you are bridged 🥳.
|
|
||||||
|
48
docs/usage/room_configuration/gitlab_project.md
Normal file
48
docs/usage/room_configuration/gitlab_project.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
GitLab Project
|
||||||
|
=================
|
||||||
|
|
||||||
|
This connection type connects a GitLab project (e.g. https://gitlab.matrix.org/matrix-org/olm) to a room.
|
||||||
|
|
||||||
|
## Setting up
|
||||||
|
|
||||||
|
To set up a connection to a GitLab project in a new room:
|
||||||
|
|
||||||
|
(NB you must have permission to bridge GitLab repositories before you can use this command, see [auth](../auth.html#gitlab).)
|
||||||
|
|
||||||
|
1. Create a new, unencrypted room. It can be public or private.
|
||||||
|
2. Invite the bridge bot (e.g. `@hookshot:example.com`).
|
||||||
|
3. Give the bridge bot moderator permissions or higher (power level 50) (or otherwise configure the room so the bot can edit room state).
|
||||||
|
4. Send the command `!hookshot gitlab project https://mydomain/my/project`.
|
||||||
|
5. If you have permission to bridge this repo, the bridge will respond with a confirmation message. (Users with `Developer` permissions or greater can bridge projects.)
|
||||||
|
6. If you have configured the bridge with a `publicUrl` inside `gitlab.webhook`, it will automatically provision the webhook for you.
|
||||||
|
7. Otherwise, you'll need to manually configure the webhook to point to your public address for the webhooks listener.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
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*|
|
||||||
|
|commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!gh`|
|
||||||
|
|pushTagsRegex|Only mention pushed tags which match this regex|Regex string|*empty*|
|
||||||
|
|prDiff|Show a diff in the room when a PR is created, subject to limits|`{enabled: boolean, maxLines: number}`|`{enabled: false}`|
|
||||||
|
|includingLabels|Only notify on issues matching these label names|Array of: String matching a label name|*empty*|
|
||||||
|
|excludingLabels|Never notify on issues matching these label names|Array of: String matching a label name|*empty*|
|
||||||
|
|
||||||
|
|
||||||
|
### Supported event types
|
||||||
|
|
||||||
|
This connection supports sending messages when the following actions happen on the repository.
|
||||||
|
|
||||||
|
- merge_request
|
||||||
|
- merge_request.close
|
||||||
|
- merge_request.merge
|
||||||
|
- merge_request.open
|
||||||
|
- merge_request.review.comments
|
||||||
|
- merge_request.review
|
||||||
|
- push
|
||||||
|
- release
|
||||||
|
- release.created
|
||||||
|
- tag_push
|
||||||
|
- wiki
|
@ -346,12 +346,11 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
|||||||
}
|
}
|
||||||
const instance = this.config.gitlab.instances[instanceName];
|
const instance = this.config.gitlab.instances[instanceName];
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return this.sendNotice("The bridge is not configured for this GitLab instance.");
|
return this.sendNotice("The bridge is not configured for this GitLab instance. Ask your administrator for a list of instances.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const client = new GitLabClient(instance.url, accessToken);
|
const client = new GitLabClient(instance.url, accessToken);
|
||||||
me = await client.user();
|
me = await client.user();
|
||||||
client.issues
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.error("Gitlab auth error:", ex);
|
log.error("Gitlab auth error:", ex);
|
||||||
return this.sendNotice("Could not authenticate with GitLab. Is your token correct?");
|
return this.sendNotice("Could not authenticate with GitLab. Is your token correct?");
|
||||||
|
@ -162,6 +162,7 @@ export interface GitLabInstance {
|
|||||||
|
|
||||||
export interface BridgeConfigGitLabYAML {
|
export interface BridgeConfigGitLabYAML {
|
||||||
webhook: {
|
webhook: {
|
||||||
|
publicUrl?: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
},
|
},
|
||||||
instances: {[name: string]: GitLabInstance};
|
instances: {[name: string]: GitLabInstance};
|
||||||
@ -171,6 +172,7 @@ export interface BridgeConfigGitLabYAML {
|
|||||||
export class BridgeConfigGitLab {
|
export class BridgeConfigGitLab {
|
||||||
readonly instances: {[name: string]: GitLabInstance};
|
readonly instances: {[name: string]: GitLabInstance};
|
||||||
readonly webhook: {
|
readonly webhook: {
|
||||||
|
publicUrl?: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -182,6 +184,15 @@ export class BridgeConfigGitLab {
|
|||||||
this.webhook = yaml.webhook;
|
this.webhook = yaml.webhook;
|
||||||
this.userIdPrefix = yaml.userIdPrefix || "_gitlab_";
|
this.userIdPrefix = yaml.userIdPrefix || "_gitlab_";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getInstanceByProjectUrl(url: string): {name: string, instance: GitLabInstance}|null {
|
||||||
|
for (const [name, instance] of Object.entries(this.instances)) {
|
||||||
|
if (url.startsWith(instance.url)) {
|
||||||
|
return {name, instance};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BridgeConfigFeeds {
|
export interface BridgeConfigFeeds {
|
||||||
|
@ -76,6 +76,7 @@ export const DefaultConfig = new BridgeConfig({
|
|||||||
},
|
},
|
||||||
webhook: {
|
webhook: {
|
||||||
secret: "secrettoken",
|
secret: "secrettoken",
|
||||||
|
publicUrl: "https://example.com/hookshot/"
|
||||||
},
|
},
|
||||||
userIdPrefix: "_gitlab_",
|
userIdPrefix: "_gitlab_",
|
||||||
},
|
},
|
||||||
|
@ -6,15 +6,18 @@ import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
|
|||||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||||
import markdown from "markdown-it";
|
import markdown from "markdown-it";
|
||||||
import LogWrapper from "../LogWrapper";
|
import LogWrapper from "../LogWrapper";
|
||||||
import { GitLabInstance } from "../Config/Config";
|
import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
|
||||||
import { IGitLabNote, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
|
import { IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
|
||||||
import { CommandConnection } from "./CommandConnection";
|
import { CommandConnection } from "./CommandConnection";
|
||||||
import { IConnectionState } from "./IConnection";
|
import { IConnectionState } from "./IConnection";
|
||||||
|
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||||
|
import { ErrCode, ApiError } from "../api"
|
||||||
|
import { AccessLevel } from "../Gitlab/Types";
|
||||||
|
|
||||||
export interface GitLabRepoConnectionState extends IConnectionState {
|
export interface GitLabRepoConnectionState extends IConnectionState {
|
||||||
instance: string;
|
instance: string;
|
||||||
path: string;
|
path: string;
|
||||||
ignoreHooks?: string[],
|
ignoreHooks?: AllowedEventsNames[],
|
||||||
commandPrefix?: string;
|
commandPrefix?: string;
|
||||||
pushTagsRegex?: string,
|
pushTagsRegex?: string,
|
||||||
includingLabels?: string[];
|
includingLabels?: string[];
|
||||||
@ -26,6 +29,69 @@ const md = new markdown();
|
|||||||
|
|
||||||
const MRRCOMMENT_DEBOUNCE_MS = 5000;
|
const MRRCOMMENT_DEBOUNCE_MS = 5000;
|
||||||
|
|
||||||
|
|
||||||
|
export type GitLabRepoResponseItem = GetConnectionsResponseItem<GitLabRepoConnectionState, undefined>;
|
||||||
|
|
||||||
|
|
||||||
|
type AllowedEventsNames =
|
||||||
|
"merge_request.open" |
|
||||||
|
"merge_request.close" |
|
||||||
|
"merge_request.merge" |
|
||||||
|
"merge_request.review" |
|
||||||
|
"merge_request.review.comments" |
|
||||||
|
`merge_request.${string}` |
|
||||||
|
"merge_request" |
|
||||||
|
"tag_push" |
|
||||||
|
"push" |
|
||||||
|
"wiki" |
|
||||||
|
`wiki.${string}` |
|
||||||
|
"release" |
|
||||||
|
"release.created";
|
||||||
|
|
||||||
|
const AllowedEvents: AllowedEventsNames[] = [
|
||||||
|
"merge_request.open",
|
||||||
|
"merge_request.close",
|
||||||
|
"merge_request.merge",
|
||||||
|
"merge_request.review",
|
||||||
|
"merge_request.review.comments",
|
||||||
|
"merge_request",
|
||||||
|
"tag_push",
|
||||||
|
"push",
|
||||||
|
"wiki",
|
||||||
|
"release",
|
||||||
|
"release.created",
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
function validateState(state: Record<string, unknown>): GitLabRepoConnectionState {
|
||||||
|
if (typeof state.instance !== "string") {
|
||||||
|
throw new ApiError("Expected a 'instance' property", ErrCode.BadValue);
|
||||||
|
}
|
||||||
|
if (typeof state.path !== "string") {
|
||||||
|
throw new ApiError("Expected a 'path' property", ErrCode.BadValue);
|
||||||
|
}
|
||||||
|
const res: GitLabRepoConnectionState = {
|
||||||
|
instance: state.instance,
|
||||||
|
path: state.path,
|
||||||
|
}
|
||||||
|
if (state.commandPrefix) {
|
||||||
|
if (typeof state.commandPrefix !== "string") {
|
||||||
|
throw new ApiError("Expected 'commandPrefix' to be a string", ErrCode.BadValue);
|
||||||
|
}
|
||||||
|
if (state.commandPrefix.length < 2 || state.commandPrefix.length > 24) {
|
||||||
|
throw new ApiError("Expected 'commandPrefix' to be between 2-24 characters", ErrCode.BadValue);
|
||||||
|
}
|
||||||
|
res.commandPrefix = state.commandPrefix;
|
||||||
|
}
|
||||||
|
if (state.ignoreHooks && Array.isArray(state.ignoreHooks)) {
|
||||||
|
if (state.ignoreHooks?.find((ev) => !AllowedEvents.includes(ev))?.length) {
|
||||||
|
throw new ApiError(`'events' can only contain ${AllowedEvents.join(", ")}`, ErrCode.BadValue);
|
||||||
|
}
|
||||||
|
res.ignoreHooks = state.ignoreHooks;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles rooms connected to a github repo.
|
* Handles rooms connected to a github repo.
|
||||||
*/
|
*/
|
||||||
@ -43,6 +109,55 @@ export class GitLabRepoConnection extends CommandConnection {
|
|||||||
|
|
||||||
private readonly debounceMRComments = new Map<string, {comments: number, author: string, timeout: NodeJS.Timeout}>();
|
private readonly debounceMRComments = new Map<string, {comments: number, author: string, timeout: NodeJS.Timeout}>();
|
||||||
|
|
||||||
|
public static async provisionConnection(roomId: string, requester: string, data: Record<string, unknown>, as: Appservice, tokenStore: UserTokenStore, instance: GitLabInstance, gitlabConfig: BridgeConfigGitLab) {
|
||||||
|
const validData = validateState(data);
|
||||||
|
const client = await tokenStore.getGitLabForUser(requester, instance.url);
|
||||||
|
if (!client) {
|
||||||
|
throw new ApiError("User is not authenticated with GitLab", ErrCode.ForbiddenUser);
|
||||||
|
}
|
||||||
|
let permissionLevel;
|
||||||
|
let project;
|
||||||
|
try {
|
||||||
|
project = await client.projects.get(validData.path);
|
||||||
|
permissionLevel = Math.max(project.permissions.group_access?.access_level || 0, project.permissions.project_access?.access_level || 0) as AccessLevel;
|
||||||
|
} catch (ex) {
|
||||||
|
throw new ApiError("Could not determine if the user has access to this project, does the project exist?", ErrCode.ForbiddenUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionLevel < AccessLevel.Developer) {
|
||||||
|
throw new ApiError("You must at least have developer access to bridge this project", ErrCode.ForbiddenUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to setup a webhook
|
||||||
|
if (gitlabConfig.webhook.publicUrl) {
|
||||||
|
const hooks = await client.projects.hooks.list(project.id);
|
||||||
|
const hasHook = hooks.find(h => h.url === gitlabConfig.webhook.publicUrl);
|
||||||
|
if (!hasHook) {
|
||||||
|
log.info(`Creating webhook for ${validData.path}`);
|
||||||
|
await client.projects.hooks.add(project.id, {
|
||||||
|
url: gitlabConfig.webhook.publicUrl,
|
||||||
|
token: gitlabConfig.webhook.secret,
|
||||||
|
enable_ssl_verification: true,
|
||||||
|
// TODO: Determine which of these actually interests the user.
|
||||||
|
issues_events: true,
|
||||||
|
merge_requests_events: true,
|
||||||
|
push_events: true,
|
||||||
|
releases_events: true,
|
||||||
|
tag_push_events: true,
|
||||||
|
wiki_page_events: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info(`Not creating webhook, webhookUrl is not defined in config`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateEventKey = `${validData.instance}/${validData.path}`;
|
||||||
|
return {
|
||||||
|
stateEventContent: validData,
|
||||||
|
connection: new GitLabRepoConnection(roomId, stateEventKey, as, validData, tokenStore, instance),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(roomId: string,
|
constructor(roomId: string,
|
||||||
stateKey: string,
|
stateKey: string,
|
||||||
private readonly as: Appservice,
|
private readonly as: Appservice,
|
||||||
@ -81,6 +196,26 @@ export class GitLabRepoConnection extends CommandConnection {
|
|||||||
return GitLabRepoConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
|
return GitLabRepoConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getProvisionerDetails(botUserId: string) {
|
||||||
|
return {
|
||||||
|
service: "gitlab",
|
||||||
|
eventType: GitLabRepoConnection.CanonicalEventType,
|
||||||
|
type: "GitLabRepo",
|
||||||
|
// TODO: Add ability to configure the bot per connnection type.
|
||||||
|
botUserId: botUserId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProvisionerDetails() {
|
||||||
|
return {
|
||||||
|
...GitLabRepoConnection.getProvisionerDetails(this.as.botUserId),
|
||||||
|
id: this.connectionId,
|
||||||
|
config: {
|
||||||
|
...this.state,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@botCommand("create", "Create an issue for this repo", ["title"], ["description", "labels"], true)
|
@botCommand("create", "Create an issue for this repo", ["title"], ["description", "labels"], true)
|
||||||
public async onCreateIssue(userId: string, title: string, description?: string, labels?: string) {
|
public async onCreateIssue(userId: string, title: string, description?: string, labels?: string) {
|
||||||
const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url);
|
const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url);
|
||||||
@ -130,7 +265,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
|||||||
|
|
||||||
public async onMergeRequestOpened(event: IGitLabWebhookMREvent) {
|
public async onMergeRequestOpened(event: IGitLabWebhookMREvent) {
|
||||||
log.info(`onMergeRequestOpened ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
log.info(`onMergeRequestOpened ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
||||||
if (this.shouldSkipHook('merge_request.open') || !this.matchesLabelFilter(event)) {
|
if (this.shouldSkipHook('merge_request', 'merge_request.open') || !this.matchesLabelFilter(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.validateMREvent(event);
|
this.validateMREvent(event);
|
||||||
@ -146,7 +281,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
|||||||
|
|
||||||
public async onMergeRequestClosed(event: IGitLabWebhookMREvent) {
|
public async onMergeRequestClosed(event: IGitLabWebhookMREvent) {
|
||||||
log.info(`onMergeRequestClosed ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
log.info(`onMergeRequestClosed ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
||||||
if (this.shouldSkipHook('merge_request.close') || !this.matchesLabelFilter(event)) {
|
if (this.shouldSkipHook('merge_request', 'merge_request.close') || !this.matchesLabelFilter(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.validateMREvent(event);
|
this.validateMREvent(event);
|
||||||
@ -162,7 +297,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
|||||||
|
|
||||||
public async onMergeRequestMerged(event: IGitLabWebhookMREvent) {
|
public async onMergeRequestMerged(event: IGitLabWebhookMREvent) {
|
||||||
log.info(`onMergeRequestMerged ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
log.info(`onMergeRequestMerged ${this.roomId} ${this.path} #${event.object_attributes.iid}`);
|
||||||
if (this.shouldSkipHook('merge_request.merge') || !this.matchesLabelFilter(event)) {
|
if (this.shouldSkipHook('merge_request', 'merge_request.merge') || !this.matchesLabelFilter(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.validateMREvent(event);
|
this.validateMREvent(event);
|
||||||
@ -177,7 +312,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async onMergeRequestReviewed(event: IGitLabWebhookMREvent) {
|
public async onMergeRequestReviewed(event: IGitLabWebhookMREvent) {
|
||||||
if (this.shouldSkipHook('merge_request.review', `merge_request.${event.object_attributes.action}`) || !this.matchesLabelFilter(event)) {
|
if (this.shouldSkipHook('merge_request', 'merge_request.review', `merge_request.${event.object_attributes.action}`) || !this.matchesLabelFilter(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(`onMergeRequestReviewed ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
log.info(`onMergeRequestReviewed ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
||||||
@ -297,7 +432,7 @@ ${data.description}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async onCommentCreated(event: IGitLabWebhookNoteEvent) {
|
public async onCommentCreated(event: IGitLabWebhookNoteEvent) {
|
||||||
if (this.shouldSkipHook('merge_request.review', 'merge_request.review.comments')) {
|
if (this.shouldSkipHook('merge_request', 'merge_request.review', 'merge_request.review.comments')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(`onCommentCreated ${this.roomId} ${this.toString()} ${event.merge_request?.iid} ${event.object_attributes.id}`);
|
log.info(`onCommentCreated ${this.roomId} ${this.toString()} ${event.merge_request?.iid} ${event.object_attributes.id}`);
|
||||||
@ -367,7 +502,7 @@ ${data.description}`;
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSkipHook(...hookName: string[]) {
|
private shouldSkipHook(...hookName: AllowedEventsNames[]) {
|
||||||
if (this.state.ignoreHooks) {
|
if (this.state.ignoreHooks) {
|
||||||
for (const name of hookName) {
|
for (const name of hookName) {
|
||||||
if (this.state.ignoreHooks?.includes(name)) {
|
if (this.state.ignoreHooks?.includes(name)) {
|
||||||
|
@ -14,6 +14,7 @@ import { FeedConnection } from "./FeedConnection";
|
|||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import { SetupWidget } from "../Widgets/SetupWidget";
|
import { SetupWidget } from "../Widgets/SetupWidget";
|
||||||
import { AdminRoom } from "../AdminRoom";
|
import { AdminRoom } from "../AdminRoom";
|
||||||
|
import { GitLabRepoConnection } from "./GitlabRepo";
|
||||||
const md = new markdown();
|
const md = new markdown();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,6 +75,33 @@ export class SetupConnection extends CommandConnection {
|
|||||||
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`);
|
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@botCommand("gitlab project", { help: "Create a connection for a GitHub project. (You must be logged in with GitLab to do this.)", requiredArgs: ["url"], includeUserId: true, category: "gitlab"})
|
||||||
|
public async onGitLabRepo(userId: string, url: string) {
|
||||||
|
if (!this.config.gitlab) {
|
||||||
|
throw new CommandError("not-configured", "The bridge is not configured to support GitLab.");
|
||||||
|
}
|
||||||
|
url = url.toLowerCase();
|
||||||
|
|
||||||
|
await this.checkUserPermissions(userId, "gitlab", GitLabRepoConnection.CanonicalEventType);
|
||||||
|
|
||||||
|
const {name, instance} = this.config.gitlab.getInstanceByProjectUrl(url) || {};
|
||||||
|
if (!instance || !name) {
|
||||||
|
throw new CommandError("not-configured", "No instance found that matches the provided URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await this.tokenStore.getGitLabForUser(userId, instance.url);
|
||||||
|
if (!client) {
|
||||||
|
throw new CommandError("User not logged in", "You are not logged into this GitLab instance. Start a DM with this bot and use the command `gitlab personaltoken`.");
|
||||||
|
}
|
||||||
|
const path = url.slice(instance.url.length + 1);
|
||||||
|
if (!path) {
|
||||||
|
throw new CommandError("Invalid GitLab url", "The GitLab project url you entered was not valid.");
|
||||||
|
}
|
||||||
|
const res = await GitLabRepoConnection.provisionConnection(this.roomId, userId, {path, instance: name}, this.as, this.tokenStore, instance, this.config.gitlab);
|
||||||
|
await this.as.botClient.sendStateEvent(this.roomId, GitLabRepoConnection.CanonicalEventType, url, res.stateEventContent);
|
||||||
|
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
@botCommand("jira project", { help: "Create a connection for a JIRA project. (You must be logged in with JIRA to do this.)", requiredArgs: ["url"], includeUserId: true, category: "jira"})
|
@botCommand("jira project", { help: "Create a connection for a JIRA project. (You must be logged in with JIRA to do this.)", requiredArgs: ["url"], includeUserId: true, category: "jira"})
|
||||||
public async onJiraProject(userId: string, urlStr: string) {
|
public async onJiraProject(userId: string, urlStr: string) {
|
||||||
const url = new URL(urlStr);
|
const url = new URL(urlStr);
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { GitLabInstance } from "../Config/Config";
|
import { GitLabInstance } from "../Config/Config";
|
||||||
import { GetIssueResponse, GetUserResponse, CreateIssueOpts, CreateIssueResponse, GetIssueOpts, EditIssueOpts, GetTodosResponse, EventsOpts, CreateIssueNoteOpts, CreateIssueNoteResponse } from "./Types";
|
import { GetIssueResponse, GetUserResponse, CreateIssueOpts, CreateIssueResponse, GetIssueOpts, EditIssueOpts, GetTodosResponse, EventsOpts, CreateIssueNoteOpts, CreateIssueNoteResponse, GetProjectResponse, ProjectHook, ProjectHookOpts } from "./Types";
|
||||||
import LogWrapper from "../LogWrapper";
|
import LogWrapper from "../LogWrapper";
|
||||||
import { URLSearchParams } from "url";
|
import { URLSearchParams } from "url";
|
||||||
import UserAgent from "../UserAgent";
|
import UserAgent from "../UserAgent";
|
||||||
|
|
||||||
const log = new LogWrapper("GitLabClient");
|
const log = new LogWrapper("GitLabClient");
|
||||||
|
|
||||||
|
type ProjectId = string|number|string[];
|
||||||
|
|
||||||
|
function getProjectId(id: ProjectId) {
|
||||||
|
return encodeURIComponent(Array.isArray(id) ? id.join("/") : id);
|
||||||
|
}
|
||||||
export class GitLabClient {
|
export class GitLabClient {
|
||||||
constructor(private instanceUrl: string, private token: string) {
|
constructor(private instanceUrl: string, private token: string) {
|
||||||
|
|
||||||
@ -55,11 +61,30 @@ export class GitLabClient {
|
|||||||
return (await axios.put(`api/v4/projects/${opts.id}/issues/${opts.issue_iid}`, opts, this.defaultConfig)).data;
|
return (await axios.put(`api/v4/projects/${opts.id}/issues/${opts.issue_iid}`, opts, this.defaultConfig)).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getProject(projectParts: string[]): Promise<GetIssueResponse> {
|
|
||||||
|
private async getProject(id: ProjectId): Promise<GetProjectResponse> {
|
||||||
try {
|
try {
|
||||||
return (await axios.get(`api/v4/projects/${projectParts.join("%2F")}`, this.defaultConfig)).data;
|
return (await axios.get(`api/v4/projects/${getProjectId(id)}`, this.defaultConfig)).data;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.warn(`Failed to get issue:`, ex);
|
log.warn(`Failed to get project:`, ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectHooks(id: ProjectId): Promise<ProjectHook[]> {
|
||||||
|
try {
|
||||||
|
return (await axios.get(`api/v4/projects/${getProjectId(id)}/hooks`, this.defaultConfig)).data;
|
||||||
|
} catch (ex) {
|
||||||
|
log.warn(`Failed to get project hooks:`, ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addProjectHook(id: ProjectId, opts: ProjectHookOpts): Promise<ProjectHook> {
|
||||||
|
try {
|
||||||
|
return (await axios.post(`api/v4/projects/${getProjectId(id)}/hooks`, opts, this.defaultConfig)).data;
|
||||||
|
} catch (ex) {
|
||||||
|
log.warn(`Failed to create project hook:`, ex);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,6 +122,10 @@ export class GitLabClient {
|
|||||||
get projects() {
|
get projects() {
|
||||||
return {
|
return {
|
||||||
get: this.getProject.bind(this),
|
get: this.getProject.bind(this),
|
||||||
|
hooks: {
|
||||||
|
list: this.getProjectHooks.bind(this),
|
||||||
|
add: this.addProjectHook.bind(this),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,18 @@ export interface GitLabAuthor {
|
|||||||
web_url: string;
|
web_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://docs.gitlab.com/ee/api/access_requests.html#valid-access-levels
|
||||||
|
export enum AccessLevel {
|
||||||
|
NoAccess = 0,
|
||||||
|
MinimalAccess = 5,
|
||||||
|
Guest = 10,
|
||||||
|
Reporter = 20,
|
||||||
|
Developer = 30,
|
||||||
|
Maintainer = 40,
|
||||||
|
// Only valid to set for groups
|
||||||
|
Owner = 50,
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetUserResponse {
|
export interface GetUserResponse {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@ -152,4 +164,58 @@ export interface CreateIssueNoteResponse {
|
|||||||
noteable_iid: string;
|
noteable_iid: string;
|
||||||
commands_changes: unknown;
|
commands_changes: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetProjectResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path_with_namespace: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
visibility: "private"|"internal"|"public",
|
||||||
|
state: 'opened'|'closed';
|
||||||
|
owner: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
permissions: {
|
||||||
|
project_access?: {
|
||||||
|
access_level: AccessLevel;
|
||||||
|
},
|
||||||
|
group_access?: {
|
||||||
|
access_level: AccessLevel;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
author: GitLabAuthor;
|
||||||
|
references: {
|
||||||
|
short: string;
|
||||||
|
relative: string;
|
||||||
|
full: string;
|
||||||
|
}
|
||||||
|
web_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectHookOpts {
|
||||||
|
url: string;
|
||||||
|
token: string;
|
||||||
|
push_events?: true;
|
||||||
|
push_events_branch_filter?: string;
|
||||||
|
issues_events?: boolean;
|
||||||
|
confidential_issues_events?: boolean;
|
||||||
|
merge_requests_events?: boolean;
|
||||||
|
tag_push_events?: boolean;
|
||||||
|
note_events?: boolean;
|
||||||
|
confidential_note_events?: boolean;
|
||||||
|
job_events?: boolean;
|
||||||
|
pipeline_events?: boolean;
|
||||||
|
wiki_page_events?: boolean;
|
||||||
|
deployment_events?: boolean;
|
||||||
|
releases_events?: boolean;
|
||||||
|
enable_ssl_verification?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectHook extends ProjectHookOpts {
|
||||||
|
id: number;
|
||||||
|
token: never;
|
||||||
|
project_id: 3;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user