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
|
||||
webhook:
|
||||
secret: secrettoken
|
||||
publicUrl: https://example.com/hookshot/
|
||||
userIdPrefix:
|
||||
# (Optional) Prefix used when creating ghost users for GitLab accounts.
|
||||
#
|
||||
|
@ -14,6 +14,7 @@
|
||||
- [Authenticating](./usage/auth.md)
|
||||
- [Room Configuration](./usage/room_configuration.md)
|
||||
- [GitHub Repo](./usage/room_configuration/github_repo.md)
|
||||
- [GitLab Project](./usage/room_configuration/gitlab_project.md)
|
||||
- [📊 Metrics](./metrics.md)
|
||||
|
||||
# 🥼 Advanced
|
||||
|
@ -12,6 +12,7 @@ GitLab configuration is fairly straight-forward:
|
||||
url: https://gitlab.com
|
||||
webhook:
|
||||
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
|
||||
@ -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
|
||||
"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 is a case of navigating to the settings page, and then adding a new webhook.
|
||||
You will want to give the URL of the public address for the hookshot webhooks port on the `/` path.
|
||||
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 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];
|
||||
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 {
|
||||
const client = new GitLabClient(instance.url, accessToken);
|
||||
me = await client.user();
|
||||
client.issues
|
||||
} catch (ex) {
|
||||
log.error("Gitlab auth error:", ex);
|
||||
return this.sendNotice("Could not authenticate with GitLab. Is your token correct?");
|
||||
|
@ -162,6 +162,7 @@ export interface GitLabInstance {
|
||||
|
||||
export interface BridgeConfigGitLabYAML {
|
||||
webhook: {
|
||||
publicUrl?: string;
|
||||
secret: string;
|
||||
},
|
||||
instances: {[name: string]: GitLabInstance};
|
||||
@ -171,6 +172,7 @@ export interface BridgeConfigGitLabYAML {
|
||||
export class BridgeConfigGitLab {
|
||||
readonly instances: {[name: string]: GitLabInstance};
|
||||
readonly webhook: {
|
||||
publicUrl?: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
@ -182,6 +184,15 @@ export class BridgeConfigGitLab {
|
||||
this.webhook = yaml.webhook;
|
||||
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 {
|
||||
|
@ -76,6 +76,7 @@ export const DefaultConfig = new BridgeConfig({
|
||||
},
|
||||
webhook: {
|
||||
secret: "secrettoken",
|
||||
publicUrl: "https://example.com/hookshot/"
|
||||
},
|
||||
userIdPrefix: "_gitlab_",
|
||||
},
|
||||
|
@ -6,15 +6,18 @@ import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
|
||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
import markdown from "markdown-it";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { GitLabInstance } from "../Config/Config";
|
||||
import { IGitLabNote, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
|
||||
import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
|
||||
import { IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
|
||||
import { CommandConnection } from "./CommandConnection";
|
||||
import { IConnectionState } from "./IConnection";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { ErrCode, ApiError } from "../api"
|
||||
import { AccessLevel } from "../Gitlab/Types";
|
||||
|
||||
export interface GitLabRepoConnectionState extends IConnectionState {
|
||||
instance: string;
|
||||
path: string;
|
||||
ignoreHooks?: string[],
|
||||
ignoreHooks?: AllowedEventsNames[],
|
||||
commandPrefix?: string;
|
||||
pushTagsRegex?: string,
|
||||
includingLabels?: string[];
|
||||
@ -26,6 +29,69 @@ const md = new markdown();
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -43,6 +109,55 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
|
||||
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,
|
||||
stateKey: string,
|
||||
private readonly as: Appservice,
|
||||
@ -81,6 +196,26 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
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)
|
||||
public async onCreateIssue(userId: string, title: string, description?: string, labels?: string) {
|
||||
const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url);
|
||||
@ -130,7 +265,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
|
||||
public async onMergeRequestOpened(event: IGitLabWebhookMREvent) {
|
||||
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;
|
||||
}
|
||||
this.validateMREvent(event);
|
||||
@ -146,7 +281,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
|
||||
public async onMergeRequestClosed(event: IGitLabWebhookMREvent) {
|
||||
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;
|
||||
}
|
||||
this.validateMREvent(event);
|
||||
@ -162,7 +297,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
|
||||
public async onMergeRequestMerged(event: IGitLabWebhookMREvent) {
|
||||
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;
|
||||
}
|
||||
this.validateMREvent(event);
|
||||
@ -177,7 +312,7 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
log.info(`onMergeRequestReviewed ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
||||
@ -297,7 +432,7 @@ ${data.description}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
log.info(`onCommentCreated ${this.roomId} ${this.toString()} ${event.merge_request?.iid} ${event.object_attributes.id}`);
|
||||
@ -367,7 +502,7 @@ ${data.description}`;
|
||||
return true;
|
||||
}
|
||||
|
||||
private shouldSkipHook(...hookName: string[]) {
|
||||
private shouldSkipHook(...hookName: AllowedEventsNames[]) {
|
||||
if (this.state.ignoreHooks) {
|
||||
for (const name of hookName) {
|
||||
if (this.state.ignoreHooks?.includes(name)) {
|
||||
|
@ -14,6 +14,7 @@ import { FeedConnection } from "./FeedConnection";
|
||||
import { URL } from "url";
|
||||
import { SetupWidget } from "../Widgets/SetupWidget";
|
||||
import { AdminRoom } from "../AdminRoom";
|
||||
import { GitLabRepoConnection } from "./GitlabRepo";
|
||||
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}`);
|
||||
}
|
||||
|
||||
@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"})
|
||||
public async onJiraProject(userId: string, urlStr: string) {
|
||||
const url = new URL(urlStr);
|
||||
|
@ -1,11 +1,17 @@
|
||||
import axios from "axios";
|
||||
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 { URLSearchParams } from "url";
|
||||
import UserAgent from "../UserAgent";
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
private async getProject(projectParts: string[]): Promise<GetIssueResponse> {
|
||||
|
||||
private async getProject(id: ProjectId): Promise<GetProjectResponse> {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -97,6 +122,10 @@ export class GitLabClient {
|
||||
get projects() {
|
||||
return {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
id: number;
|
||||
username: string;
|
||||
@ -153,3 +165,57 @@ export interface CreateIssueNoteResponse {
|
||||
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