Skip to content

Commit

Permalink
Add BitBucket webhook parser and statuses (#210)
Browse files Browse the repository at this point in the history
* Add BitBucket webhook recordings

* Create bitbucket webhook URL

* Add Cypress test for bitbucket

* Add BitBucket push webhook parser

* Setup BitBucket build parser stump

* Process BitBucket pipeline builds

* Set proper process name and clean up readthedocs status determining

* Test succesfull pipeline too

* Create bitbucket pull request parser stub

* Add BitBucket pull request parser and add documentation

* Bump CIMonitor version to 4.14.0
  • Loading branch information
rick-nu authored Jul 12, 2024
1 parent 49d9dbb commit 518ff06
Show file tree
Hide file tree
Showing 29 changed files with 2,202 additions and 83 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ deployments are successful. All in one overview. This is all done via **webhooks
- [GitHub](https://cimonitor.readthedocs.io/en/latest/webhook/github/)
- [GitLab](https://cimonitor.readthedocs.io/en/latest/webhook/gitlab/)
- [Read the Docs](https://cimonitor.readthedocs.io/en/latest/webhook/readthedocs/)
- [BitBucket](https://cimonitor.readthedocs.io/en/latest/webhook/bitbucket/)

## Example

Expand Down
101 changes: 101 additions & 0 deletions backend/parser/bitbucket/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { isOldProcess } from 'backend/status/helper';
import StatusManager from 'backend/status/manager';
import { BitBucketBuildState, BitBucketCommitStatusWebhook } from 'types/bitbucket';
import Status, { Process, State, StepAndStageState } from 'types/status';

class BitBucketBuildParser {
parse(id: string, build: BitBucketCommitStatusWebhook): Status {
let status = StatusManager.getStatus(id);

if (!status) {
status = {
id,
project: `${build.repository.workspace.name} / ${build.repository.name}`,
state: 'info',
source: 'bitbucket',
time: new Date().toUTCString(),
processes: [],
};
}

let processes: Process[] = status.processes || [];

const processId = parseInt(build.commit_status.key);

if (!processes.find((process) => process.id === processId)) {
if (isOldProcess(status, processId)) {
return null;
}

processes.push({
id: processId,
title: build.commit_status.commit.message,
state: 'warning',
stages: [],
time: new Date().toUTCString(),
});
}

processes = processes.map((process) => {
if (process.id === processId) {
return this.patchProcess(process, build);
}

return process;
});

const commitUser = build.commit_status.commit.author.user;
return {
...status,
processes,
username: commitUser.display_name,
userUrl: commitUser.links.html.href,
userImage: commitUser.links.avatar.href,
projectImage: build.repository.links.avatar.href,
sourceUrl: build.repository.links.html.href,
time: new Date().toUTCString(),
};
}

patchProcess(process: Process, build: BitBucketCommitStatusWebhook): Process {
return {
...process,
stages: [
{
id: 'build',
steps: [],
time: new Date().toUTCString(),
state: this.getStageState(build.commit_status.state),
title: build.commit_status.name,
},
],
state: this.getProcessState(build.commit_status.state),
};
}

getProcessState(state: BitBucketBuildState): State {
if (state === 'SUCCESSFUL') {
return 'success';
}

if (state === 'FAILED') {
return 'error';
}

return 'warning';
}

getStageState(state: BitBucketBuildState): StepAndStageState {
if (state === 'SUCCESSFUL') {
return 'success';
}

if (state === 'FAILED') {
return 'failed';
}

return 'running';
}
}

export default new BitBucketBuildParser();
68 changes: 68 additions & 0 deletions backend/parser/bitbucket/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Slugify from 'backend/parser/slug';
import {
BitBucketChangeWrapper,
BitBucketCommitStatusWebhook,
BitBucketPullRequestWebhook,
BitBucketPushWebhook,
BitBucketRepository,
} from 'types/bitbucket';
import Status from 'types/status';

import BitBucketBuildParser from './build';
import BitBucketPullRequestParser from './pull-request';
import BitBucketPushParser from './push';

class BitBucketParser {
getInternalId(repository: BitBucketRepository, branch: string): string {
return `bitbucket-${Slugify(repository.full_name)}-${Slugify(branch)}`;
}

parsePush(push: BitBucketPushWebhook): Status {
console.log('[parser/bitbucket] Parsing push...');

const relevantChange = push.push.changes.find((changes: BitBucketChangeWrapper) => {
if (changes.new) {
return changes.new.type === 'branch' || changes.new.type === 'tag';
}

return false;
});

if (!relevantChange) {
console.log('[parser/bitbucket] No relevant change of type branch was found. Stopping.');
return null;
}

const id = this.getInternalId(push.repository, relevantChange.new.name);

return BitBucketPushParser.parse(id, push, relevantChange.new);
}

parseBuild(build: BitBucketCommitStatusWebhook): Status {
console.log('[parser/bitbucket] Parsing build...');

if (build.commit_status.refname === null) {
console.log('[parser/bitbucket] Build could not be linked to a branch. Stopping.');
return null;
}

if (parseInt(build.commit_status.key) === 0 || isNaN(parseInt(build.commit_status.key))) {
console.log('[parser/bitbucket] Build has an invalid key, should be a build number. Stopping.');
return null;
}

const id = this.getInternalId(build.repository, build.commit_status.refname);

return BitBucketBuildParser.parse(id, build);
}

parsePullRequest(pr: BitBucketPullRequestWebhook): Status {
console.log('[parser/bitbucket] Parsing pull request...');

const id = this.getInternalId(pr.repository, pr.pullrequest.source.branch.name);

return BitBucketPullRequestParser.parse(id, pr);
}
}

export default new BitBucketParser();
36 changes: 36 additions & 0 deletions backend/parser/bitbucket/pull-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import StatusManager from 'backend/status/manager';
import { BitBucketPullRequestWebhook } from 'types/bitbucket';
import Status from 'types/status';

class BitBucketPullRequestParser {
parse(id: string, pr: BitBucketPullRequestWebhook): Status {
let status = StatusManager.getStatus(id);

if (!status) {
status = {
id,
project: `${pr.repository.workspace.name} / ${pr.repository.name}`,
state: 'info',
source: 'bitbucket',
time: new Date().toUTCString(),
processes: [],
};
}

const commitUser = pr.actor;
return {
...status,
branch: pr.pullrequest.source.branch.name,
username: commitUser.display_name,
userUrl: commitUser.links.html.href,
userImage: commitUser.links.avatar.href,
mergeTitle: pr.pullrequest.title,
mergeUrl: pr.pullrequest.links.html.href,
projectImage: pr.repository.links.avatar.href,
sourceUrl: pr.repository.links.html.href,
time: new Date().toUTCString(),
};
}
}

export default new BitBucketPullRequestParser();
41 changes: 41 additions & 0 deletions backend/parser/bitbucket/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import StatusManager from 'backend/status/manager';
import { BitBucketChange, BitBucketPushWebhook } from 'types/bitbucket';
import Status from 'types/status';

class BitBucketPushParser {
parse(id: string, push: BitBucketPushWebhook, change: BitBucketChange): Status {
let status = StatusManager.getStatus(id);

if (!status) {
status = {
id,
project: `${push.repository.workspace.name} / ${push.repository.name}`,
state: 'info',
source: 'bitbucket',
time: new Date().toUTCString(),
processes: [],
};

if (change.type === 'branch') {
status.branch = change.name;
}

if (change.type === 'tag') {
status.tag = change.name;
}
}

const commitUser = change.target.author.user;
return {
...status,
username: commitUser.display_name,
userUrl: commitUser.links.html.href,
userImage: commitUser.links.avatar.href,
projectImage: push.repository.links.avatar.href,
sourceUrl: push.repository.links.html.href,
time: new Date().toUTCString(),
};
}
}

export default new BitBucketPushParser();
4 changes: 2 additions & 2 deletions backend/parser/github/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GitHubConclusion, GitHubStatus } from 'types/github';
import { State, StepState } from 'types/status';
import { State, StepAndStageState } from 'types/status';

export const getProcessState = (status: GitHubStatus, conclusion: GitHubConclusion): State => {
if (conclusion !== null) {
Expand All @@ -17,7 +17,7 @@ export const getProcessState = (status: GitHubStatus, conclusion: GitHubConclusi
return 'info';
};

export const getStepState = (status: GitHubStatus, conclusion: GitHubConclusion): StepState => {
export const getStepState = (status: GitHubStatus, conclusion: GitHubConclusion): StepAndStageState => {
if (conclusion !== null) {
if (conclusion === 'failure') {
return 'failed';
Expand Down
4 changes: 2 additions & 2 deletions backend/parser/github/job.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Slugify from 'backend/parser/slug';
import StatusManager from 'backend/status/manager';
import { GitHubWorkflowJob } from 'types/github';
import Status, { Process, Stage, Step, StepState } from 'types/status';
import Status, { Process, Stage, Step, StepAndStageState } from 'types/status';

import { getStepState } from './helper';

Expand Down Expand Up @@ -104,7 +104,7 @@ class GitHubJobParser {
};
}

getStageState(steps: Step[]): StepState {
getStageState(steps: Step[]): StepAndStageState {
if (steps.length === 0) {
return 'running';
}
Expand Down
4 changes: 2 additions & 2 deletions backend/parser/gitlab/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Slugify from 'backend/parser/slug';
import { isOldProcess } from 'backend/status/helper';
import StatusManager from 'backend/status/manager';
import { GitLabBuild } from 'types/gitlab';
import Status, { Process, Stage, Step, StepState } from 'types/status';
import Status, { Process, Stage, Step, StepAndStageState } from 'types/status';

import { statusToStepState } from './helper';

Expand Down Expand Up @@ -147,7 +147,7 @@ class GitLabBuildParser {
};
}

determineStageState(steps: Step[]): StepState {
determineStageState(steps: Step[]): StepAndStageState {
if (steps.find((step) => ['failed'].includes(step.state))) {
return 'failed';
}
Expand Down
6 changes: 3 additions & 3 deletions backend/parser/gitlab/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GitLabStatus } from 'types/gitlab';
import { State, StepState } from 'types/status';
import { State, StepAndStageState } from 'types/status';

type GitLabStatusMapper<ExpectedState> = {
// eslint-disable-next-line no-unused-vars
Expand All @@ -19,8 +19,8 @@ export const statusToState = (status: GitLabStatus): State => {
return states[status] || 'info';
};

export const statusToStepState = (status: GitLabStatus, allowFailure: boolean): StepState => {
const states: GitLabStatusMapper<StepState> = {
export const statusToStepState = (status: GitLabStatus, allowFailure: boolean): StepAndStageState => {
const states: GitLabStatusMapper<StepAndStageState> = {
pending: 'pending',
running: 'running',
created: 'created',
Expand Down
Loading

0 comments on commit 518ff06

Please sign in to comment.