mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 05:05:53 +00:00
Merge pull request #149 from MTRNord/circleci
[UNTESTED] Add CircleCI Integration
This commit is contained in:
commit
83244fae37
8
config/integrations/circleci.yaml
Normal file
8
config/integrations/circleci.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
type: "complex-bot"
|
||||
integrationType: "circleci"
|
||||
enabled: false # disabled because the API is considered unstable/inoperable. Use at your own risk!
|
||||
name: "CircleCI"
|
||||
about: "Sends CircleCI build results into the room"
|
||||
avatar: "img/avatars/circleci.png"
|
||||
upstream:
|
||||
type: "vector"
|
53
src/integration/impl/circleci/CircleCiBot.js
Normal file
53
src/integration/impl/circleci/CircleCiBot.js
Normal file
@ -0,0 +1,53 @@
|
||||
var ComplexBot = require("../../generic_types/ComplexBot");
|
||||
|
||||
/**
|
||||
* Represents a CircleCI bot
|
||||
*/
|
||||
class CircleCiBot extends ComplexBot {
|
||||
|
||||
/**
|
||||
* Creates a new CircleCI bot
|
||||
* @param botConfig the bot configuration
|
||||
* @param backbone the backbone powering this bot
|
||||
*/
|
||||
constructor(botConfig, backbone) {
|
||||
super(botConfig);
|
||||
this._backbone = backbone;
|
||||
}
|
||||
|
||||
/*override*/
|
||||
getUserId() {
|
||||
return this._backbone.getUserId();
|
||||
}
|
||||
|
||||
/*override*/
|
||||
getState() {
|
||||
var response = {
|
||||
repoTemplates: [],
|
||||
immutableRepoTemplates: [],
|
||||
webhookUrl: ""
|
||||
};
|
||||
return this._backbone.getRepos().then(templates => {
|
||||
response.repoTemplates = templates;
|
||||
return this._backbone.getImmutableRepos();
|
||||
}).then(immutable => {
|
||||
response.immutableRepoTemplates = immutable;
|
||||
return this._backbone.getWebhookUrl();
|
||||
}).then(url => {
|
||||
response.webhookUrl = url;
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/*override*/
|
||||
removeFromRoom(roomId) {
|
||||
return this._backbone.removeFromRoom(roomId);
|
||||
}
|
||||
|
||||
/*override*/
|
||||
updateState(newState) {
|
||||
return this._backbone.setRepos(newState.repoTemplates).then(() => this.getState());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CircleCiBot;
|
20
src/integration/impl/circleci/CircleCiFactory.js
Normal file
20
src/integration/impl/circleci/CircleCiFactory.js
Normal file
@ -0,0 +1,20 @@
|
||||
var CircleCiBot = require("./CircleCiBot");
|
||||
var VectorCircleCiBackbone = require("./VectorCircleCiBackbone");
|
||||
var UpstreamConfiguration = require("../../../UpstreamConfiguration");
|
||||
|
||||
var factory = (db, integrationConfig, roomId, scalarToken) => {
|
||||
factory.validateConfig(integrationConfig);
|
||||
|
||||
return db.getUpstreamToken(scalarToken).then(upstreamToken => {
|
||||
var backbone = new VectorCircleCiBackbone(roomId, upstreamToken);
|
||||
return new CircleCiBot(integrationConfig, backbone);
|
||||
});
|
||||
};
|
||||
|
||||
factory.validateConfig = (integrationConfig) => {
|
||||
if (!integrationConfig.upstream) throw new Error("Unsupported configuration");
|
||||
if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream");
|
||||
if (!UpstreamConfiguration.hasUpstream("vector")) throw new Error("Vector upstream not specified");
|
||||
};
|
||||
|
||||
module.exports = factory;
|
63
src/integration/impl/circleci/StubbedCircleCiBackbone.js
Normal file
63
src/integration/impl/circleci/StubbedCircleCiBackbone.js
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Stubbed/placeholder CircleCI backbone
|
||||
*/
|
||||
class StubbedCircleCiBackbone {
|
||||
|
||||
/**
|
||||
* Creates a new stubbed CircleCI backbone
|
||||
*/
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user ID for this backbone
|
||||
* @returns {Promise<string>} resolves to the user ID
|
||||
*/
|
||||
getUserId() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the repository templates for this backbone
|
||||
* @returns {Promise<{repoKey:string,template:string}[]>} resolves to the collection of repositories and their templates
|
||||
*/
|
||||
getRepos() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the immutable repository templates for this backbone (set by other users)
|
||||
* @returns {Promise<{repoKey:string,template:string,ownerId:string}[]>} resolves to the collection of repositories and their templates
|
||||
*/
|
||||
getImmutableRepos() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the new repository templates for this backbone
|
||||
* @param {{repoKey:string,template:string}[]} newRepos the new templates for the repositories
|
||||
* @returns {Promise<>} resolves when complete
|
||||
*/
|
||||
setRepos(newRepos) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the webhook url for this backbone
|
||||
* @returns {Promise<string>} resolves to the webhook URL
|
||||
*/
|
||||
getWebhookUrl() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the bot from the given room
|
||||
* @param {string} roomId the room ID to remove the bot from
|
||||
* @returns {Promise<>} resolves when completed
|
||||
*/
|
||||
removeFromRoom(roomId) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StubbedCircleCiBackbone;
|
108
src/integration/impl/circleci/VectorCircleCiBackbone.js
Normal file
108
src/integration/impl/circleci/VectorCircleCiBackbone.js
Normal file
@ -0,0 +1,108 @@
|
||||
var StubbedCircleCiBackbone = require("./StubbedCircleCiBackbone");
|
||||
var VectorScalarClient = require("../../../scalar/VectorScalarClient");
|
||||
var _ = require("lodash");
|
||||
var log = require("../../../util/LogService");
|
||||
|
||||
/**
|
||||
* Backbone for CircleCI bots running on vector.im through scalar
|
||||
*/
|
||||
class VectorCircleCiBackbone extends StubbedCircleCiBackbone {
|
||||
|
||||
/**
|
||||
* Creates a new Vector CircleCI backbone
|
||||
* @param {string} roomId the room ID to manage
|
||||
* @param {string} upstreamScalarToken the vector scalar token
|
||||
*/
|
||||
constructor(roomId, upstreamScalarToken) {
|
||||
super();
|
||||
this._roomId = roomId;
|
||||
this._scalarToken = upstreamScalarToken;
|
||||
this._info = null;
|
||||
this._otherTemplates = [];
|
||||
}
|
||||
|
||||
/*override*/
|
||||
getUserId() {
|
||||
return (this._info ? Promise.resolve() : this._getInfo()).then(() => {
|
||||
return this._info.bot_user_id;
|
||||
});
|
||||
}
|
||||
|
||||
/*override*/
|
||||
getRepos() {
|
||||
return (this._info ? Promise.resolve() : this._getInfo()).then(() => {
|
||||
if (this._info.integrations.length == 0) return [];
|
||||
|
||||
var rooms = _.keys(this._info.integrations[0].config.rooms);
|
||||
if (rooms.indexOf(this._roomId) === -1) return [];
|
||||
|
||||
var repos = _.keys(this._info.integrations[0].config.rooms[this._roomId].repos);
|
||||
return _.map(repos, r => {
|
||||
return {repoKey: r, template: this._info.integrations[0].config.rooms[this._roomId].repos[r].template};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*override*/
|
||||
getImmutableRepos() {
|
||||
return (this._info ? Promise.resolve() : this._getInfo()).then(() => {
|
||||
return this._otherTemplates;
|
||||
});
|
||||
}
|
||||
|
||||
/*override*/
|
||||
setRepos(newRepos) {
|
||||
var config = {};
|
||||
config[this._roomId] = {repos: {}};
|
||||
for (var repo of newRepos) config[this._roomId].repos[repo.repoKey] = {template: repo.template};
|
||||
|
||||
return VectorScalarClient.configureIntegration("circleci", this._scalarToken, {
|
||||
rooms: config
|
||||
});
|
||||
}
|
||||
|
||||
/*override*/
|
||||
getWebhookUrl() {
|
||||
// string
|
||||
return (this._info ? Promise.resolve() : this._getInfo()).then(() => {
|
||||
if (this._info.integrations.length == 0) return "";
|
||||
return this._info.integrations[0].config.webhook_url;
|
||||
});
|
||||
}
|
||||
|
||||
_getInfo() {
|
||||
return VectorScalarClient.getIntegrationsForRoom(this._roomId, this._scalarToken).then(integrations => {
|
||||
this._otherTemplates = [];
|
||||
for (var integration of integrations) {
|
||||
if (integration.self) continue; // skip - we're not looking for ones we know about
|
||||
if (integration.type == "circleci") {
|
||||
var roomIds = _.keys(integration.config.rooms);
|
||||
if (roomIds.length === 0) continue;
|
||||
if (roomIds.length !== 1) log.warn("VectorCircleCiBackbone", "Expected 1 room but found " + roomIds.length);
|
||||
|
||||
var roomConfig = integration.config.rooms[roomIds[0]];
|
||||
var repositories = _.keys(roomConfig.repos);
|
||||
|
||||
for (var repo of repositories) {
|
||||
this._otherTemplates.push({
|
||||
repoKey: repo,
|
||||
template: roomConfig.repos[repo].template,
|
||||
ownerId: integration.user_id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return VectorScalarClient.getIntegration("circleci", this._roomId, this._scalarToken);
|
||||
}).then(info => {
|
||||
this._info = info;
|
||||
});
|
||||
}
|
||||
|
||||
/*override*/
|
||||
removeFromRoom(roomId) {
|
||||
return VectorScalarClient.removeIntegration("circleci", roomId, this._scalarToken);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VectorCircleCiBackbone;
|
@ -4,12 +4,14 @@ var SimpleBotFactory = require("./simple_bot/SimpleBotFactory");
|
||||
var RSSFactory = require("./rss/RSSFactory");
|
||||
var IRCFactory = require("./irc/IRCFactory");
|
||||
var TravisCiFactory = require("./travisci/TravisCiFactory");
|
||||
var CircleCiFactory = require("./circleci/CircleCiFactory");
|
||||
var SimpleWidgetFactory = require("./simple_widget/SimpleWidgetFactory");
|
||||
|
||||
var mapping = {
|
||||
"complex-bot": {
|
||||
"rss": RSSFactory,
|
||||
"travisci": TravisCiFactory,
|
||||
"circleci": CircleCiFactory,
|
||||
},
|
||||
"bridge": {
|
||||
"irc": IRCFactory,
|
||||
|
63
web/app/configs/circleci/circleci-config.component.html
Normal file
63
web/app/configs/circleci/circleci-config.component.html
Normal file
@ -0,0 +1,63 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure CircleCI hooks</h4>
|
||||
</div>
|
||||
<div class="config-content">
|
||||
<form (submit)="addRepository()" novalidate name="addRepoForm">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 12px;">
|
||||
<h6>.circleci/config.yml configuration</h6>
|
||||
The following will need to be added to your .circleci/config.yml file:
|
||||
<pre class="yaml">{{ circleYaml }}</pre>
|
||||
</div>
|
||||
<div class="col-md-8" style="margin-bottom: 12px;">
|
||||
<h6>Your CircleCI hooks</h6>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="owner/repo-name"
|
||||
[(ngModel)]="repoKey" name="repoKey"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Repository
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable" *ngFor="let repo of integration.repoTemplates trackById">
|
||||
{{ repo.repoKey }}
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="editTemplate(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating" *ngIf="!isTemplateToggled(repo.repoKey)">
|
||||
<i class="fa fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeRepository(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Repository
|
||||
</button>
|
||||
<div *ngIf="isTemplateToggled(repo.repoKey)">
|
||||
<textarea [(ngModel)]="repo.newTemplate" name="template-{{repo.repoKey}}"
|
||||
style="width: 100%; height: 100px; margin-top: 5px;"></textarea>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="saveTemplate(repo.repoKey)">Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleTemplate(repo.repoKey)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" *ngIf="integration.immutableRepoTemplates.length > 0">
|
||||
<h6 class="other-items-title">Hooks from other users in the room</h6>
|
||||
</div>
|
||||
<div class="col-md-12 list" *ngFor="let repo of integration.immutableRepoTemplates trackById">
|
||||
{{ repo.repoKey }} <span class="text-muted">(added by {{ repo.ownerId }})</span>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="toggleTemplate(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
{{ isTemplateToggled(repo.repoKey) ? "Hide" : "Show" }} Template
|
||||
</button>
|
||||
<pre *ngIf="isTemplateToggled(repo.repoKey)">{{ repo.template }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
19
web/app/configs/circleci/circleci-config.component.scss
Normal file
19
web/app/configs/circleci/circleci-config.component.scss
Normal file
@ -0,0 +1,19 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.list {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.removable {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.other-items-title {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.yaml {
|
||||
border: 1px solid #ccc;
|
||||
background: #eee;
|
||||
padding: 5px;
|
||||
}
|
116
web/app/configs/circleci/circleci-config.component.ts
Normal file
116
web/app/configs/circleci/circleci-config.component.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { CircleCiIntegration } from "../../shared/models/integration";
|
||||
import { ModalComponent, DialogRef } from "ngx-modialog";
|
||||
import { ConfigModalContext } from "../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { ApiService } from "../../shared/api.service";
|
||||
|
||||
@Component({
|
||||
selector: "my-circleci-config",
|
||||
templateUrl: "./circleci-config.component.html",
|
||||
styleUrls: ["./circleci-config.component.scss", "./../config.component.scss"],
|
||||
})
|
||||
export class CircleCiConfigComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
public integration: CircleCiIntegration;
|
||||
|
||||
public isUpdating = false;
|
||||
public repoKey = "";
|
||||
public repoTemplate = "";
|
||||
public circleYaml = "";
|
||||
|
||||
private roomId: string;
|
||||
private scalarToken: string;
|
||||
private knownRepos: string[] = [];
|
||||
private visibleTemplates = [];
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
private toaster: ToasterService,
|
||||
private api: ApiService) {
|
||||
this.integration = <CircleCiIntegration>dialog.context.integration;
|
||||
this.roomId = dialog.context.roomId;
|
||||
this.scalarToken = dialog.context.scalarToken;
|
||||
|
||||
this.circleYaml = "notify:\n webhooks:\n - url: " + this.integration.webhookUrl;
|
||||
|
||||
this.calculateKnownRepos();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private calculateKnownRepos() {
|
||||
for (let repo of this.integration.repoTemplates)
|
||||
this.knownRepos.push(repo.repoKey);
|
||||
for (let immutableRepo of this.integration.immutableRepoTemplates)
|
||||
this.knownRepos.push(immutableRepo.repoKey);
|
||||
}
|
||||
|
||||
public toggleTemplate(repoKey: string) {
|
||||
let idx = this.visibleTemplates.indexOf(repoKey);
|
||||
if (idx === -1) this.visibleTemplates.push(repoKey);
|
||||
else this.visibleTemplates.splice(idx, 1);
|
||||
}
|
||||
|
||||
public isTemplateToggled(repoKey: string) {
|
||||
return this.visibleTemplates.indexOf(repoKey) !== -1;
|
||||
}
|
||||
|
||||
public editTemplate(repoKey: string) {
|
||||
this.toggleTemplate(repoKey);
|
||||
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
|
||||
repoConfig.newTemplate = repoConfig.template;
|
||||
}
|
||||
|
||||
public saveTemplate(repoKey: string) {
|
||||
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
|
||||
repoConfig.template = repoConfig.newTemplate;
|
||||
this.updateTemplates().then(() => this.toggleTemplate(repoKey));
|
||||
}
|
||||
|
||||
public addRepository() {
|
||||
if (!this.repoKey || this.repoKey.trim().length === 0) {
|
||||
this.toaster.pop("warning", "Please enter a repository");
|
||||
return;
|
||||
}
|
||||
if (this.knownRepos.indexOf(this.repoKey) !== -1) {
|
||||
this.toaster.pop("error", "Repository " + this.repoKey + " is already being tracked");
|
||||
return;
|
||||
}
|
||||
|
||||
this.integration.repoTemplates.push({repoKey: this.repoKey, template: this.repoTemplate, newTemplate: ""});
|
||||
this.updateTemplates().then(() => this.reset());
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.repoKey = "";
|
||||
this.repoTemplate = "%{build_num}#%{build_num} (%{branch} - %{commit} : %{committer_name}): %{outcome}\n Build details : %{build_url}\n";
|
||||
}
|
||||
|
||||
public removeRepository(repoKey: string) {
|
||||
for (let i = 0; i < this.integration.repoTemplates.length; i++) {
|
||||
if (this.integration.repoTemplates[i].repoKey === repoKey) {
|
||||
this.integration.repoTemplates.splice(i, 1);
|
||||
this.updateTemplates().then(() => this.reset());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.toaster.pop("error", "Could not find target repository");
|
||||
}
|
||||
|
||||
public updateTemplates() {
|
||||
this.isUpdating = true;
|
||||
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
|
||||
repoTemplates: this.integration.repoTemplates
|
||||
}).then(response => {
|
||||
this.integration.repoTemplates = response.repoTemplates;
|
||||
this.integration.immutableRepoTemplates = response.immutableRepoTemplates;
|
||||
this.calculateKnownRepos();
|
||||
this.isUpdating = false;
|
||||
this.toaster.pop("success", "Repositories updated");
|
||||
}).catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import { RssConfigComponent } from "../configs/rss/rss-config.component";
|
||||
import { ContainerContent } from "ngx-modialog";
|
||||
import { IrcConfigComponent } from "../configs/irc/irc-config.component";
|
||||
import { TravisCiConfigComponent } from "../configs/travisci/travisci-config.component";
|
||||
import { CircleCiConfigComponent } from "../configs/circleci/circleci-config.component";
|
||||
import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/custom_widget-config.component";
|
||||
import { YoutubeWidgetConfigComponent } from "../configs/widget/youtube/youtube-config.component";
|
||||
import { TwitchWidgetConfigComponent } from "../configs/widget/twitch/twitch-config.component";
|
||||
@ -28,6 +29,9 @@ export class IntegrationService {
|
||||
"travisci": {
|
||||
component: TravisCiConfigComponent,
|
||||
},
|
||||
"circleci": {
|
||||
component: CircleCiConfigComponent,
|
||||
},
|
||||
},
|
||||
"bridge": {
|
||||
"irc": {
|
||||
|
@ -26,6 +26,12 @@ export interface TravisCiIntegration extends Integration {
|
||||
webhookUrl: string; // immutable
|
||||
}
|
||||
|
||||
export interface CircleCiIntegration extends Integration {
|
||||
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
|
||||
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
|
||||
webhookUrl: string; // immutable
|
||||
}
|
||||
|
||||
export interface IRCIntegration extends Integration {
|
||||
availableNetworks: { name: string, id: string }[];
|
||||
channels: { [networkId: string]: string[] };
|
||||
|
BIN
web/public/img/avatars/circleci.png
Normal file
BIN
web/public/img/avatars/circleci.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Loading…
Reference in New Issue
Block a user