mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 01:05:53 -04:00
Support Travis CI completely
Webhooks are generated eagerly but have lazily set targets so that we can give the user a webhook URL to set in their config. go-neb (upstream & standalone) doesn't give a webhook URL until after the repositories are configured. Fixes #16
This commit is contained in:
parent
6f238fc13f
commit
4365cb0753
35
src/api/dimension/DimensionWebhookService.ts
Normal file
35
src/api/dimension/DimensionWebhookService.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { FormParam, HeaderParam, Path, PathParam, POST } from "typescript-rest";
|
||||
import Webhook from "../../db/models/Webhook";
|
||||
import { ApiError } from "../ApiError";
|
||||
import * as request from "request";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
|
||||
@Path("/api/v1/dimension/webhooks")
|
||||
export class DimensionWebhookService {
|
||||
|
||||
@POST
|
||||
@Path("/travisci/:webhookId")
|
||||
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
|
||||
const webhook = await Webhook.findByPrimary(webhookId).catch(() => null);
|
||||
if (!webhook) throw new ApiError(404, "Webhook not found");
|
||||
if (!webhook.targetUrl) throw new ApiError(400, "Webhook not configured");
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
request({
|
||||
method: "POST",
|
||||
url: webhook.targetUrl,
|
||||
form: {payload: payload},
|
||||
headers: {
|
||||
"Signature": signature,
|
||||
},
|
||||
}, (err, res, _body) => {
|
||||
if (err) {
|
||||
LogService.error("DimensionWebhooksService", "Error invoking travis-ci webhook");
|
||||
LogService.error("DimensionWebhooksService", res.body);
|
||||
|
||||
throw new ApiError(500, "Internal Server Error");
|
||||
} else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ import NebIntegration from "./models/NebIntegration";
|
||||
import NebBotUser from "./models/NebBotUser";
|
||||
import NebNotificationUser from "./models/NebNotificationUser";
|
||||
import NebIntegrationConfig from "./models/NebIntegrationConfig";
|
||||
import Webhook from "./models/Webhook";
|
||||
|
||||
class _DimensionStore {
|
||||
private sequelize: Sequelize;
|
||||
@ -39,6 +40,7 @@ class _DimensionStore {
|
||||
NebBotUser,
|
||||
NebNotificationUser,
|
||||
NebIntegrationConfig,
|
||||
Webhook,
|
||||
]);
|
||||
}
|
||||
|
||||
|
22
src/db/migrations/20180330111645-AddWebhooks.ts
Normal file
22
src/db/migrations/20180330111645-AddWebhooks.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_webhooks", {
|
||||
"hookId": {type: DataType.STRING, primaryKey: true, allowNull: false},
|
||||
"ownerUserId": {
|
||||
type: DataType.STRING, allowNull: false,
|
||||
references: {model: "dimension_users", key: "userId"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
"purposeId": {type: DataType.STRING, allowNull: false},
|
||||
"targetUrl": {type: DataType.STRING, allowNull: true},
|
||||
}));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.dropTable("dimension_webhooks"));
|
||||
}
|
||||
}
|
25
src/db/models/Webhook.ts
Normal file
25
src/db/models/Webhook.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||
import User from "./User";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_webhooks",
|
||||
underscoredAll: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class Webhook extends Model<Webhook> {
|
||||
// This is really just a holding class to keep foreign keys alive
|
||||
|
||||
@PrimaryKey
|
||||
@Column
|
||||
hookId: string;
|
||||
|
||||
@Column
|
||||
@ForeignKey(() => User)
|
||||
ownerUserId: string;
|
||||
|
||||
@Column
|
||||
purposeId: string;
|
||||
|
||||
@Column
|
||||
targetUrl: string;
|
||||
}
|
@ -21,7 +21,7 @@ export interface RssBotConfiguration {
|
||||
}
|
||||
|
||||
export interface TravisCiConfiguration {
|
||||
webhookUrl: string;
|
||||
webhookId: string;
|
||||
repos: {
|
||||
[repoKey: string]: {
|
||||
addedByUserId: string;
|
||||
|
@ -11,6 +11,8 @@ import { AppserviceStore } from "../db/AppserviceStore";
|
||||
import { MatrixAppserviceClient } from "../matrix/MatrixAppserviceClient";
|
||||
import NebIntegrationConfig from "../db/models/NebIntegrationConfig";
|
||||
import { RssBotConfiguration, TravisCiConfiguration } from "../integrations/ComplexBot";
|
||||
import Webhook from "../db/models/Webhook";
|
||||
import * as randomString from "random-string";
|
||||
|
||||
interface InternalTravisCiConfig {
|
||||
webhookUrl: string;
|
||||
@ -65,18 +67,17 @@ export class NebProxy {
|
||||
public async getServiceConfiguration(integration: NebIntegration, inRoomId: string): Promise<any> {
|
||||
if (integration.nebId !== this.neb.id) throw new Error("Integration is not for this NEB proxy");
|
||||
|
||||
let result = null;
|
||||
if (this.neb.upstreamId) {
|
||||
try {
|
||||
const response = await this.doUpstreamRequest<ModularIntegrationInfoResponse>("/integrations/" + NebClient.getNebType(integration.type), {
|
||||
room_id: inRoomId,
|
||||
});
|
||||
|
||||
if (integration.type === "rss") return this.parseUpstreamRssConfiguration(response.integrations);
|
||||
else if (integration.type === "travisci") return this.parseUpstreamTravisCiConfiguration(response.integrations);
|
||||
else return {};
|
||||
if (integration.type === "rss") result = await this.parseUpstreamRssConfiguration(response.integrations);
|
||||
else if (integration.type === "travisci") result = await this.parseUpstreamTravisCiConfiguration(response.integrations);
|
||||
} catch (err) {
|
||||
LogService.error("NebProxy", err);
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
const serviceConfig = await NebIntegrationConfig.findOne({
|
||||
@ -85,8 +86,17 @@ export class NebProxy {
|
||||
roomId: inRoomId,
|
||||
},
|
||||
});
|
||||
return serviceConfig ? JSON.parse(serviceConfig.jsonContent) : {};
|
||||
result = serviceConfig ? JSON.parse(serviceConfig.jsonContent) : {};
|
||||
}
|
||||
|
||||
if (!result) result = {};
|
||||
if (integration.type === "travisci") {
|
||||
// Replace the webhook ID with the requesting user's webhook ID (generating it if needed)
|
||||
result["webhookId"] = await this.getWebhookId(integration.type);
|
||||
delete result["webhook_url"];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async setServiceConfiguration(integration: NebIntegration, inRoomId: string, newConfig: any): Promise<any> {
|
||||
@ -135,7 +145,7 @@ export class NebProxy {
|
||||
private parseUpstreamTravisCiConfiguration(integrations: any[]): InternalTravisCiConfig {
|
||||
if (!integrations) return {rooms: {}, webhookUrl: null};
|
||||
|
||||
const result: InternalTravisCiConfig = {rooms: {}, webhookUrl: "https://example.org/nowhere"};
|
||||
const result: InternalTravisCiConfig = {rooms: {}, webhookUrl: null};
|
||||
for (const integration of integrations) {
|
||||
if (!integration.user_id || !integration.config || !integration.config.rooms) continue;
|
||||
|
||||
@ -240,14 +250,24 @@ export class NebProxy {
|
||||
}
|
||||
|
||||
if (this.neb.upstreamId) {
|
||||
await this.doUpstreamRequest<ModularIntegrationInfoResponse>("/integrations/travis-ci/configureService", {
|
||||
await this.doUpstreamRequest("/integrations/travis-ci/configureService", {
|
||||
room_id: roomId,
|
||||
rooms: newConfig.rooms,
|
||||
});
|
||||
|
||||
// Annoyingly, we don't get any kind of feedback for the webhook - we have to re-request it
|
||||
const response = await this.doUpstreamRequest<ModularIntegrationInfoResponse>("/integrations/travis-ci", {
|
||||
room_id: roomId,
|
||||
});
|
||||
const parsed = this.parseUpstreamTravisCiConfiguration(response.integrations);
|
||||
if (parsed && parsed.webhookUrl)
|
||||
await this.setWebhookTarget("travisci", parsed.webhookUrl);
|
||||
} else {
|
||||
const client = new NebClient(this.neb);
|
||||
const notifUser = await NebStore.getOrCreateNotificationUser(this.neb.id, "travisci", this.requestingUserId);
|
||||
await client.setServiceConfig(notifUser.serviceId, notifUser.appserviceUserId, "travis-ci", newConfig);
|
||||
const result = await client.setServiceConfig(notifUser.serviceId, notifUser.appserviceUserId, "travis-ci", newConfig);
|
||||
if (result['NewConfig'] && result['NewConfig']['webhook_url'])
|
||||
await this.setWebhookTarget("travisci", result['NewConfig']['webhook_url']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,6 +283,35 @@ export class NebProxy {
|
||||
}
|
||||
}
|
||||
|
||||
private async getWebhookId(serviceId: string): Promise<string> {
|
||||
// We add a bit of uniqueness to the service ID to avoid conflicts
|
||||
serviceId = serviceId + "_" + this.neb.id;
|
||||
|
||||
let webhook = await Webhook.findOne({
|
||||
where: {
|
||||
purposeId: serviceId,
|
||||
ownerUserId: this.requestingUserId
|
||||
}
|
||||
}).catch(() => null);
|
||||
if (!webhook) {
|
||||
webhook = await Webhook.create({
|
||||
hookId: randomString({length: 160}),
|
||||
ownerUserId: this.requestingUserId,
|
||||
purposeId: serviceId,
|
||||
targetUrl: null, // Will be populated later
|
||||
});
|
||||
}
|
||||
|
||||
return webhook.hookId;
|
||||
}
|
||||
|
||||
private async setWebhookTarget(serviceId: string, targetUrl: string): Promise<any> {
|
||||
const webhookId = await this.getWebhookId(serviceId);
|
||||
const webhook = await Webhook.findByPrimary(webhookId);
|
||||
webhook.targetUrl = targetUrl;
|
||||
return webhook.save();
|
||||
}
|
||||
|
||||
private async doUpstreamRequest<T>(endpoint: string, body?: any): Promise<T> {
|
||||
const upstream = await Upstream.findByPrimary(this.neb.upstreamId);
|
||||
const token = await UserScalarToken.findOne({
|
||||
|
@ -1,6 +1,4 @@
|
||||
Release checklist:
|
||||
* RSS bot
|
||||
* Travis CI
|
||||
* IRC Bridge
|
||||
* Update documentation
|
||||
* Configuration migration (if possible)
|
||||
|
@ -1,5 +1,36 @@
|
||||
<my-complex-bot-config [botComponent]="this">
|
||||
<ng-template #botParamsTemplate>
|
||||
<my-ibox [isCollapsible]="true">
|
||||
<h5 class="my-ibox-title">
|
||||
.travis.yml configuration and template information
|
||||
</h5>
|
||||
<div class="my-ibox-content">
|
||||
<p>The following section needs to be added to your <code>.travis.yml</code> file in your repositories:</p>
|
||||
<pre>{{ travisYaml }}</pre>
|
||||
|
||||
<p>
|
||||
The following variables can be used in your template. This template is used to post a message to the
|
||||
room when your webhook is activated.
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>{{ '%{repository_slug}' }}</code> - The repository identifier (eg: "matrix-org/synapse")</li>
|
||||
<li><code>{{ '%{repository_name}' }}</code> - The repository name (eg: "synapse")</li>
|
||||
<li><code>{{ '%{build_number}' }}</code> - The build number</li>
|
||||
<li><code>{{ '%{build_id}' }}</code> - The build ID</li>
|
||||
<li><code>{{ '%{branch}' }}</code> - The branch name</li>
|
||||
<li><code>{{ '%{commit}' }}</code> - A short version of the commit SHA</li>
|
||||
<li><code>{{ '%{commit_subject}' }}</code> - The first line of the commit message</li>
|
||||
<li><code>{{ '%{commit_message}' }}</code> - The full commit message</li>
|
||||
<li><code>{{ '%{author}' }}</code> - The author of the commit</li>
|
||||
<li><code>{{ '%{result}' }}</code> - The result of the build</li>
|
||||
<li><code>{{ '%{message}' }}</code> - The message from Travis CI about the build</li>
|
||||
<li><code>{{ '%{duration}' }}</code> - The total duration of all builds in the matrix</li>
|
||||
<li><code>{{ '%{elapsed_timed}' }}</code> - The time it took to run the build</li>
|
||||
<li><code>{{ '%{compare_url}' }}</code> - A URL to see the changes which triggered the build</li>
|
||||
<li><code>{{ '%{build_url}' }}</code> - A URL to see the build information</li>
|
||||
</ul>
|
||||
</div>
|
||||
</my-ibox>
|
||||
<my-ibox>
|
||||
<h5 class="my-ibox-title">
|
||||
Repositories
|
||||
@ -19,7 +50,7 @@
|
||||
<tr *ngFor="let repo of getRepos()">
|
||||
<td>{{ repo.repoKey }}</td>
|
||||
<td>
|
||||
<textarea title="Repository Template" class="repo-template form-control" rows="3" (change)="repo.template = $event.target.value">{{ repo.template }}</textarea>
|
||||
<textarea title="Repository Template" class="repo-template form-control" rows="3" (change)="repo.template = $event.target.value" [disabled]="isUpdating || !repo.isSelf">{{ repo.template }}</textarea>
|
||||
</td>
|
||||
<td>{{ repo.addedByUserId }}</td>
|
||||
<td class="actions-col">
|
||||
|
@ -3,7 +3,7 @@ import { Component } from "@angular/core";
|
||||
import { SessionStorage } from "../../../shared/SessionStorage";
|
||||
|
||||
interface TravisCiConfig {
|
||||
webhookUrl: string; // TODO: Display webhook URL somewhere
|
||||
webhookId: string;
|
||||
repos: {
|
||||
[repoKey: string]: { // "turt2live/matrix-dimension"
|
||||
addedByUserId: string;
|
||||
@ -31,13 +31,36 @@ export class TravisCiComplexBotConfigComponent extends ComplexBotComponent<Travi
|
||||
super("travisci");
|
||||
}
|
||||
|
||||
public get webhookUrl(): string {
|
||||
if (!this.newConfig) return "not specified";
|
||||
|
||||
return window.location.origin + "/api/v1/dimension/webhooks/travisci/" + this.newConfig.webhookId;
|
||||
}
|
||||
|
||||
public get travisYaml(): string {
|
||||
return "" +
|
||||
"notifications:\n" +
|
||||
" webhooks:\n" +
|
||||
" urls:\n" +
|
||||
" - " + this.webhookUrl + "\n" +
|
||||
" on_success: change # always | never | change\n" +
|
||||
" on_failure: always\n" +
|
||||
" on_start: never\n";
|
||||
}
|
||||
|
||||
public addRepo(): void {
|
||||
if (!this.newRepoKey.trim()) {
|
||||
this.toaster.pop('warning', 'Please enter a repository');
|
||||
return;
|
||||
}
|
||||
|
||||
this.newConfig.repos[this.newRepoKey] = {addedByUserId: SessionStorage.userId, template: "TODO: Default template"};
|
||||
this.newConfig.repos[this.newRepoKey] = {
|
||||
addedByUserId: SessionStorage.userId,
|
||||
template: "" +
|
||||
"%{repository_slug}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\n" +
|
||||
" Change view : %{compare_url}\n" +
|
||||
" Build details : %{build_url}\n"
|
||||
};
|
||||
this.newRepoKey = "";
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user