You have contributed to Kibibit! We really appreciate it!
Accept this achievement as gift on my daughter's wedding day
",
7 | "name": "The Godfather Consigliere",
8 | "relatedPullRequest": "test",
9 | "short": "Great men are not born great, they contribute to Kibibit . . .",
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/achievements/src/__snapshots__/mr-miyagi.achievement.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`mrMiyagi achievement should be granted to PR creator if coverage increased to 100% 1`] = `
4 | Object {
5 | "avatar": "images/achievements/mrMiyagi.achievement.jpg",
6 | "description": "You're the ultimate zen master. You increased a project coverage to 100%. It was a long journey... but you know... First learn stand, then learn fly. Nature rule, creator-san, not mine",
7 | "name": "Mr Miyagi",
8 | "relatedPullRequest": "test",
9 | "short": "Never put passion in front of principle, even if you win, you’ll lose",
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/.github/checkgroup.yml:
--------------------------------------------------------------------------------
1 | subprojects:
2 | - id: Server
3 | paths:
4 | - "server/**"
5 | checks:
6 | - "Run linters Server"
7 | - "Server Unit Tests"
8 | - "API Tests"
9 | - "codecov/project"
10 | - "codecov/project/unit-test-server"
11 | - "codecov/project/api-tests"
12 | - id: Client
13 | paths:
14 | - "clients/**"
15 | checks:
16 | - "Client Unit Tests"
17 | - "codecov/project"
18 | - "codecov/project/unit-test-client"
19 | - id: Achievements
20 | paths:
21 | - "achievements/**"
22 | checks:
23 | - "Run linters Achievements"
24 | - "codecov/project"
25 | - "codecov/project/unit-test-achievements"
--------------------------------------------------------------------------------
/server/src/api/webhook-event-manager/webhook-event-manager.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { PullRequestModule } from '../pull-request/pull-request.module';
4 | import { RepoModule } from '../repo/repo.module';
5 | import { UserModule } from '../user/user.module';
6 | import {
7 | WebhookEventManagerController
8 | } from './webhook-event-manager.controller';
9 | import { WebhookEventManagerService } from './webhook-event-manager.service';
10 |
11 | @Module({
12 | imports: [ UserModule, RepoModule, PullRequestModule ],
13 | controllers: [WebhookEventManagerController],
14 | providers: [
15 | WebhookEventManagerService
16 | ]
17 | })
18 | export class WebhookEventManagerModule {}
19 |
--------------------------------------------------------------------------------
/server/src/app/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | import { Response } from 'express';
4 |
5 | import { Controller, Get, Res } from '@nestjs/common';
6 | import { ApiOkResponse, ApiOperation } from '@nestjs/swagger';
7 |
8 | import { ConfigService } from '@kb-config';
9 |
10 | @Controller()
11 | export class AppController {
12 | constructor(private readonly configService: ConfigService) {}
13 |
14 | @Get()
15 | @ApiOperation({ summary: 'Get Web Client (HTML)' })
16 | @ApiOkResponse({
17 | description: 'Returns the Web Client\'s HTML File'
18 | })
19 | sendWebClient(@Res() res: Response): void {
20 | res.sendFile(join(this.configService.appRoot, '/dist/client/index.html'));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/client/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { browser, logging } from 'protractor';
2 |
3 | import { AppPage } from './app.po';
4 |
5 | describe('workspace-project App', () => {
6 | let page: AppPage;
7 |
8 | beforeEach(() => {
9 | page = new AppPage();
10 | });
11 |
12 | it('should display welcome message', () => {
13 | page.navigateTo();
14 | expect(page.getTitleText()).toEqual('kibibit Client Side');
15 | });
16 |
17 | afterEach(async () => {
18 | // Assert that there are no errors emitted from the browser
19 | const logs = await browser.manage().logs().get(logging.Type.BROWSER);
20 | expect(logs).not.toContain(jasmine.objectContaining({
21 | level: logging.Level.SEVERE
22 | } as logging.Entry));
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/server/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Module } from '@nestjs/common';
3 | import { ScheduleModule } from '@nestjs/schedule';
4 |
5 | import { WinstonModule } from '@kibibit/nestjs-winston';
6 |
7 | import { ApiModule } from '@kb-api';
8 | import { ConfigModule } from '@kb-config';
9 | import { EventsGateway, EventsModule } from '@kb-events';
10 | import { TasksModule } from '@kb-tasks';
11 |
12 | import { AppController } from './app.controller';
13 |
14 | @Module({
15 | imports: [
16 | WinstonModule.forRoot({}),
17 | ApiModule,
18 | ScheduleModule.forRoot(),
19 | EventsModule,
20 | ConfigModule,
21 | TasksModule
22 | ],
23 | controllers: [AppController],
24 | providers: [EventsGateway]
25 | })
26 | export class AppModule {}
27 |
--------------------------------------------------------------------------------
/server/src/dev-tools/in-memory-database.module.ts:
--------------------------------------------------------------------------------
1 | import { MongoMemoryServer } from 'mongodb-memory-server';
2 | import mongoose from 'mongoose';
3 |
4 | import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose';
5 |
6 | let mongod: MongoMemoryServer;
7 |
8 | export const createInMemoryDatabaseModule =
9 | (options: MongooseModuleOptions = {}) => MongooseModule.forRootAsync({
10 | useFactory: async () => {
11 | mongod = new MongoMemoryServer();
12 | const mongoUri = await mongod.getUri();
13 | return {
14 | uri: mongoUri,
15 | ...options
16 | };
17 | }
18 | });
19 |
20 | export const closeInMemoryDatabaseConnection = async () => {
21 | await mongoose.disconnect();
22 | if (mongod) await mongod.stop();
23 | };
24 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | // "rbbit.typescript-hero",
4 | "coenraads.bracket-pair-colorizer",
5 | "wix.vscode-import-cost",
6 | "orta.vscode-jest",
7 | "dbaeumer.vscode-eslint",
8 | "actboy168.tasks",
9 | "johnpapa.vscode-peacock",
10 | "angular.ng-template",
11 | "abhijoybasak.nestjs-files",
12 | "eamodio.gitlens",
13 | "codeandstuff.package-json-upgrade",
14 | "mongodb.mongodb-vscode",
15 | "ms-azuretools.vscode-docker",
16 | "jock.svg",
17 | "firsttris.vscode-jest-runner",
18 | "wakatime.vscode-wakatime",
19 | "cschleiden.vscode-github-actions",
20 | "hirse.vscode-ungit",
21 | "github.vscode-pull-request-github",
22 | "wayou.vscode-todo-highlight",
23 | "mhutchie.git-graph"
24 | ]
25 | }
--------------------------------------------------------------------------------
/client/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4 |
5 | import {
6 | AngularMaterialModule
7 | } from './angular-material/angular-material.module';
8 | import { AppRoutingModule } from './app-routing.module';
9 | import { AppComponent } from './app.component';
10 |
11 | @NgModule({
12 | declarations: [
13 | AppComponent
14 | ],
15 | imports: [
16 | BrowserModule,
17 | AppRoutingModule,
18 | BrowserAnimationsModule,
19 | AngularMaterialModule
20 | ],
21 | providers: [],
22 | bootstrap: [AppComponent],
23 | schemas: [CUSTOM_ELEMENTS_SCHEMA]
24 | })
25 | export class AppModule { }
26 |
--------------------------------------------------------------------------------
/server/src/decorators/kb-post.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | applyDecorators,
3 | ClassSerializerInterceptor,
4 | Post,
5 | UseInterceptors
6 | } from '@nestjs/common';
7 | import { ApiCreatedResponse, ApiOperation } from '@nestjs/swagger';
8 |
9 | import { KbApiValidateErrorResponse } from '@kb-decorators';
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
12 | export function KbPost(type: any, path?: string | string[]) {
13 | return applyDecorators(
14 | Post(path),
15 | ApiOperation({ summary: `Create a new ${ type.name }` }),
16 | ApiCreatedResponse({
17 | description: `The ${ type.name } has been successfully created.`,
18 | type
19 | }),
20 | KbApiValidateErrorResponse(),
21 | UseInterceptors(ClassSerializerInterceptor)
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/api/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Automatically generated by barrelsby.
3 | */
4 |
5 | export * from './api.controller';
6 | export * from './api.module';
7 | export * from './pull-request/pull-request.controller';
8 | export * from './pull-request/pull-request.module';
9 | export * from './pull-request/pull-request.service';
10 | export * from './repo/repo.controller';
11 | export * from './repo/repo.module';
12 | export * from './repo/repo.service';
13 | export * from './user/user.controller';
14 | export * from './user/user.module';
15 | export * from './user/user.service';
16 | export * from './webhook-event-manager/webhook-event-manager.controller';
17 | export * from './webhook-event-manager/webhook-event-manager.module';
18 | export * from './webhook-event-manager/webhook-event-manager.service';
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Description
2 | 🚨 Review the [guidelines for contributing](../CONTRIBUTING.md) to this repository. 🚨
3 |
4 | Please explain the changes you made here.
5 |
6 | You can explain individual changes as a list:
7 |
8 | - [ ] feature name: extra details
9 | - [ ] bug: extra details (resolves #`issue_number`)
10 |
11 | ### Checklist
12 | Please check if your PR fulfills the following requirements:
13 | - [ ] Code compiles correctly (`npm run build`)
14 | - [ ] Code is linted
15 | - [ ] Created tests which fail without the change (if possible)
16 | - All **relevant** tests are passing
17 | - [ ] Server Unit Tests
18 | - [ ] Client Unit Tests
19 | - [ ] Achievements Unit Tests
20 | - [ ] API Tests
21 | - [ ] E2E Tests
22 | - [ ] Extended the README / documentation, if necessary
23 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment:
2 | show_carryforward_flags: true
3 | # Setting coverage targets per flag
4 | coverage:
5 | status:
6 | patch: off
7 | project:
8 | default:
9 | target: 80% #overall project/ repo coverage
10 | api-tests:
11 | threshold: 5%
12 | target: auto
13 | flags:
14 | - api-test
15 | unit-test-server:
16 | threshold: 5%
17 | target: auto
18 | flags:
19 | - unit-test-server
20 | unit-test-client:
21 | threshold: 5%
22 | target: auto
23 | flags:
24 | - unit-test-client
25 | unit-test-achievements:
26 | threshold: 5%
27 | target: auto
28 | flags:
29 | - unit-test-achievements
30 |
31 | flag_management:
32 | default_rules:
33 | carryforward: true
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a comment.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in
5 | # the repo. Unless a later match takes precedence,
6 | # @global-owner1 and @global-owner2 will be requested for
7 | # review when someone opens a pull request.
8 | * @thatkookooguy @ortichon @dunaevsky @zimgil
9 |
10 | # Server Owners
11 | /server/ @thatkookooguy
12 |
13 | # Client Owners
14 | /client/ @thatkookooguy
15 |
16 | # Achievements Owners
17 | # These are the people that understand the pattern to create
18 | # and work with achievements
19 | /achievements/ @thatkookooguy @ortichon @dunaevsky @zimgil
20 |
21 | # Build\Github Actions Owners
22 | /tools/ @thatkookooguy
23 | /.github/ @thatkookooguy
24 | /.devcontainer/ @thatkookooguy
25 | /.vscode/ @thatkookooguy
26 |
--------------------------------------------------------------------------------
/server/src/decorators/task-health-check.decorator.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | // import { SetMetadata } from '@nestjs/common';
4 |
5 | export const TaskHealthCheck = function (healthCheckId?: string) {
6 | return function (
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | target: any,
9 | propertyKey: string,
10 | descriptor: PropertyDescriptor
11 | ) {
12 | const originalMethod = descriptor.value;
13 | descriptor.value = async function(...args) {
14 | await originalMethod.apply(this, args);
15 | if (healthCheckId) {
16 | await pingHealthCheck(healthCheckId);
17 | }
18 | };
19 |
20 | return descriptor;
21 | };
22 | };
23 |
24 | async function pingHealthCheck(healthCheckId: string) {
25 | await axios.get(`https://hc-ping.com/${ healthCheckId }`);
26 | }
27 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/achievements/src/dev-tools/mocks.ts:
--------------------------------------------------------------------------------
1 | export class Shall {
2 | grantedAchievements: { [username: string]: any };
3 |
4 | grant(username, achievementObject) {
5 | this.grantedAchievements = this.grantedAchievements || {};
6 | this.grantedAchievements[username] = achievementObject;
7 | }
8 | }
9 |
10 | export class PullRequest {
11 | title = 'this is a happy little title';
12 | id = 'test';
13 | number: number;
14 | url = 'url';
15 | organization: { username: string };
16 | description = '';
17 | creator = {
18 | username: 'creator'
19 | };
20 | reviewers = [ {
21 | 'username': 'reviewer'
22 | } ];
23 | merged: any;
24 | reviews: any;
25 | comments: any[];
26 | inlineComments: any[];
27 | reactions: any[];
28 | commits: any[];
29 | labels: string[];
30 | createdOn: Date;
31 | files: { name: string }[];
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Client
6 |
7 |
11 |
16 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.github/workflows/add-pr-deploy-badge.yml:
--------------------------------------------------------------------------------
1 | name: Add PR Deploy Badge
2 | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows
3 | on: [deployment_status]
4 |
5 | jobs:
6 | badge:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | pull-requests: write
10 | # only runs this job on successful deploy
11 | if: github.event.deployment_status.state == 'success'
12 | steps:
13 | - name: Kb Pull Request Deployment Badges
14 | uses: kibibit/kb-badger-action@v2
15 | with:
16 | github-token: ${{secrets.GITHUB_TOKEN}}
17 | badge-left: demo
18 | badge-right: application
19 | badge-logo: heroku
20 | badge-path: api
21 | badge2-left: demo
22 | badge2-right: api-docs
23 | badge2-color: 85EA2D
24 | badge2-logo: swagger
25 | badge2-path: api/docs
26 |
--------------------------------------------------------------------------------
/server/src/decorators/get-one.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | applyDecorators,
3 | ClassSerializerInterceptor,
4 | Get,
5 | UseInterceptors
6 | } from '@nestjs/common';
7 | import {
8 | ApiBadRequestResponse,
9 | ApiNotFoundResponse,
10 | ApiOkResponse,
11 | ApiOperation
12 | } from '@nestjs/swagger';
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | export function GetOne(type: any, path?: string | string[]) {
16 | return applyDecorators(
17 | Get(path),
18 | ApiOperation({ summary: `Get an existing ${ type.name }` }),
19 | ApiOkResponse({ description: `Return a single ${ type.name }`, type }),
20 | ApiNotFoundResponse({ description: `${ type.name } not found` }),
21 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }),
22 | UseInterceptors(ClassSerializerInterceptor)
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads
2 | // recursively all the .spec and framework files
3 | import 'zone.js/dist/zone-testing';
4 |
5 | import { getTestBed } from '@angular/core/testing';
6 | import {
7 | BrowserDynamicTestingModule,
8 | platformBrowserDynamicTesting
9 | } from '@angular/platform-browser-dynamic/testing';
10 |
11 |
12 | declare const require: {
13 | context(path: string, deep?: boolean, filter?: RegExp): {
14 | keys(): string[];
15 | (id: string): T;
16 | };
17 | };
18 |
19 | // First, initialize the Angular testing environment.
20 | getTestBed().initTestEnvironment(
21 | BrowserDynamicTestingModule,
22 | platformBrowserDynamicTesting()
23 | );
24 | // Then we find all the tests.
25 | const context = require.context('./', true, /\.spec\.ts$/);
26 | // And load the modules.
27 | context.keys().map(context);
28 |
--------------------------------------------------------------------------------
/server/src/api/pull-request/pull-request.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, UseFilters } from '@nestjs/common';
2 | import { ApiTags } from '@nestjs/swagger';
3 |
4 | import { GetAll } from '@kb-decorators';
5 | import { KbValidationExceptionFilter } from '@kb-filters';
6 | import { PullRequest } from '@kb-models';
7 |
8 | import { PullRequestService } from './pull-request.service';
9 |
10 | @Controller('api/pull-request')
11 | @ApiTags('Pull Request')
12 | @UseFilters(new KbValidationExceptionFilter())
13 | export class PullRequestController {
14 | constructor(private prService: PullRequestService) { }
15 |
16 | @GetAll(PullRequest)
17 | async getAll(): Promise {
18 | const allPRs = await this.prService.findAllAsync();
19 | const allPRsParsed = allPRs.map((dbPR) => new PullRequest(dbPR.toObject()));
20 |
21 | return allPRsParsed;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/api/pull-request/pull-request.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { noop } from 'lodash';
2 |
3 | import { Test, TestingModule } from '@nestjs/testing';
4 |
5 | import { PullRequestController } from './pull-request.controller';
6 | import { PullRequestService } from './pull-request.service';
7 |
8 | describe('PullRequestController', () => {
9 | let controller: PullRequestController;
10 |
11 | beforeEach(async () => {
12 | const module: TestingModule = await Test.createTestingModule({
13 | controllers: [PullRequestController],
14 | providers: [{
15 | provide: PullRequestService,
16 | useValue: { findAllRepos: noop, findByName: noop }
17 | }]
18 | }).compile();
19 |
20 | controller = module.get(PullRequestController);
21 | });
22 |
23 | it('should be defined', () => {
24 | expect(controller).toBeDefined();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/server/src/decorators/kb-delete.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | applyDecorators,
3 | ClassSerializerInterceptor,
4 | Delete,
5 | UseInterceptors
6 | } from '@nestjs/common';
7 | import {
8 | ApiBadRequestResponse,
9 | ApiNotFoundResponse,
10 | ApiOkResponse,
11 | ApiOperation
12 | } from '@nestjs/swagger';
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | export function KbDelete(type: any, path?: string | string[]) {
16 | return applyDecorators(
17 | Delete(path),
18 | ApiOperation({
19 | summary: `Delete an existing ${ type.name }`
20 | }),
21 | ApiOkResponse({ type: type, description: `${ type.name } deleted` }),
22 | ApiNotFoundResponse({
23 | description: `${ type.name } not found`
24 | }),
25 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }),
26 | UseInterceptors(ClassSerializerInterceptor)
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/achievements/src/__snapshots__/never-go-full-retard.achievement.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`neverGoFullRetard achievement should be granted for all supported files 1`] = `
4 | Object {
5 | "avatar": "images/achievements/neverGoFullRetard.achievement.png",
6 | "description": "merged a pull request containing only pictures. pretty!",
7 | "name": "never go full retard",
8 | "relatedPullRequest": "test",
9 | "short": "Nigga, You Just Went Full Retard",
10 | }
11 | `;
12 |
13 | exports[`neverGoFullRetard achievement should be granted to creator and reviewers 1`] = `
14 | Object {
15 | "avatar": "images/achievements/neverGoFullRetard.achievement.png",
16 | "description": "merged a pull request containing only pictures. pretty!",
17 | "name": "never go full retard",
18 | "relatedPullRequest": "test",
19 | "short": "Nigga, You Just Went Full Retard",
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/achievements/src/label-baby-junior.achievement.ts:
--------------------------------------------------------------------------------
1 | import { IAchievement } from './achievement.abstract';
2 |
3 | export const labelBabyJunior: IAchievement = {
4 | name: 'Label Baby Junior',
5 | check: function(pullRequest, shall) {
6 | if (isManyLabels(pullRequest)) {
7 | const achievement = {
8 | avatar: 'images/achievements/labelBabyJunior.achievement.jpg',
9 | name: 'The Label Maker',
10 | short: 'Is this a label maker?',
11 | description: [
12 | 'You\'ve put many labels, thank you for organizing. ',
13 | 'You\'re a gift that keeps on re-giving'
14 | ].join(''),
15 | relatedPullRequest: pullRequest.id
16 | };
17 |
18 | shall.grant(pullRequest.creator.username, achievement);
19 | }
20 | }
21 | };
22 |
23 | function isManyLabels(pullRequest) {
24 | const labels = pullRequest.labels;
25 | return labels && labels.length > 5;
26 | }
27 |
--------------------------------------------------------------------------------
/client/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
19 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build Production
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | - beta
9 |
10 | jobs:
11 | build:
12 | name: Build Production
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout commit
16 | uses: actions/checkout@v2
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: 12
21 | - name: Cache node modules
22 | uses: actions/cache@v2
23 | env:
24 | cache-name: cache-node-modules
25 | with:
26 | path: '**/node_modules'
27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
28 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }}
29 | - name: Install Dependencies
30 | run: npm install
31 | - name: Build
32 | run: npm run build
--------------------------------------------------------------------------------
/server/src/api/repo/repo.service.ts:
--------------------------------------------------------------------------------
1 | import { ReturnModelType } from '@typegoose/typegoose';
2 |
3 | import { Injectable } from '@nestjs/common';
4 | import { InjectModel } from '@nestjs/mongoose';
5 |
6 | import { BaseService } from '@kb-abstracts';
7 | import { Repo } from '@kb-models';
8 |
9 | @Injectable()
10 | export class RepoService extends BaseService {
11 | constructor(
12 | @InjectModel(Repo.modelName)
13 | private readonly repoModel: ReturnModelType
14 | ) {
15 | super(repoModel, Repo);
16 | }
17 |
18 | async findAllRepos(): Promise {
19 | const dbRepos = await this.findAll().exec();
20 |
21 | return dbRepos.map((repo) => new Repo(repo.toObject()));
22 | }
23 |
24 | async findByName(name: string): Promise {
25 | const dbRepo = await this.findOne({ name }).exec();
26 |
27 | if (!dbRepo) {
28 | return;
29 | }
30 |
31 | return new Repo(dbRepo.toObject());
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/env.schema.json:
--------------------------------------------------------------------------------
1 | {"properties":{"port":{"description":"Set server port","type":"number"},"dbUrl":{"description":"DB connection URL. Expects a mongodb db for connections","format":"url","type":"string"},"webhookProxyUrl":{"description":"Used to create a custom repeatable smee webhook url instead of Generating a random one","format":"url","type":"string","pattern":"^https:\\/\\/(?:www\\.)?smee\\.io\\/[a-zA-Z0-9_-]+\\/?"},"webhookDestinationUrl":{"description":"proxy should sent events to this url for achievibit","pattern":"^([\\w]+)?(\\/[\\w-]+)*$","type":"string"},"saveToFile":{"description":"Create a file made out of the internal config. This is mostly for merging command line, environment, and file variables to a single instance","type":"boolean"},"deletePRsHealthId":{"description":"cron job monitoring id","type":"string"},"githubAccessToken":{"description":"GitHub Access Token","type":"string"}},"type":"object","required":["port","webhookProxyUrl","webhookDestinationUrl","saveToFile"]}
2 |
--------------------------------------------------------------------------------
/server/src/api/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { ReturnModelType } from '@typegoose/typegoose';
2 |
3 | import { Injectable } from '@nestjs/common';
4 | import { InjectModel } from '@nestjs/mongoose';
5 |
6 | import { BaseService } from '@kb-abstracts';
7 | import { User } from '@kb-models';
8 |
9 | @Injectable()
10 | export class UserService extends BaseService {
11 | constructor(
12 | @InjectModel(User.modelName)
13 | private readonly userModel: ReturnModelType
14 | ) {
15 | super(userModel, User);
16 | }
17 |
18 | async findAllUsers(): Promise {
19 | const dbUsers = await this.findAll().exec();
20 |
21 | return dbUsers.map((user) => new User(user.toObject()));
22 | }
23 |
24 | async findByUsername(username: string): Promise {
25 | const dbUser = await this.findOne({ username }).exec();
26 |
27 | if (!dbUser) {
28 | return;
29 | }
30 |
31 | return new User(dbUser.toObject());
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/achievements/src/member.achievement.spec.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | import { PullRequest, Shall } from './dev-tools/mocks';
4 | import { member } from './member.achievement';
5 |
6 | describe('member achievement', () => {
7 | it('should not be granted if PR opened less than 2 weeks ago', () => {
8 | const testShall = new Shall();
9 | const pullRequest = new PullRequest();
10 |
11 | pullRequest.createdOn = moment().subtract(13, 'days').toDate();
12 |
13 | member.check(pullRequest, testShall);
14 | expect(testShall.grantedAchievements).toBeUndefined();
15 | });
16 |
17 | it('should be granted if PR opened more than 2 weeks ago', () => {
18 | const testShall = new Shall();
19 | const pullRequest = new PullRequest();
20 |
21 | pullRequest.createdOn = moment().subtract(15, 'days').toDate();
22 |
23 | member.check(pullRequest, testShall);
24 | expect(testShall.grantedAchievements.creator).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/server/src/api/webhook-event-manager/webhook-event-manager.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import {
4 | WebhookEventManagerController
5 | } from './webhook-event-manager.controller';
6 | import { WebhookEventManagerService } from './webhook-event-manager.service';
7 |
8 | describe('WebhookEventManagerController', () => {
9 | let controller: WebhookEventManagerController;
10 |
11 | beforeEach(async () => {
12 | const module: TestingModule = await Test.createTestingModule({
13 | providers: [{
14 | provide: WebhookEventManagerService,
15 | useValue: {}
16 | }],
17 | controllers: [WebhookEventManagerController]
18 | }).compile();
19 |
20 | controller = module
21 | .get(WebhookEventManagerController);
22 | });
23 |
24 | it('should be defined', () => {
25 | expect(controller).toBeDefined();
26 | });
27 |
28 | it.todo('add more tests...');
29 | });
30 |
--------------------------------------------------------------------------------
/achievements/src/cutting-edges.achievement.ts:
--------------------------------------------------------------------------------
1 | import { some } from 'lodash';
2 |
3 | import { IAchievement, IUserAchievement } from './achievement.abstract';
4 |
5 | export const cuttingEdge: IAchievement = {
6 | name: 'Cutting Edges',
7 | check: function(pullRequest, shall) {
8 | if (pullRequest.merged) {
9 | const anyApprovals = some(pullRequest.reviews, function(review) {
10 | return review.state === 'APPROVED';
11 | });
12 |
13 | if (!anyApprovals) {
14 | const achieve: IUserAchievement = {
15 | avatar: 'images/achievements/cuttingEdges.achievement.jpg',
16 | name: 'Cutting Edges',
17 | short: 'Cutting corners? I also like to live dangerously',
18 | description:
19 | 'You\'ve merged a pull request without a reviewer confirming',
20 | relatedPullRequest: pullRequest.id
21 | };
22 |
23 | shall.grant(pullRequest.creator.username, achieve);
24 | }
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/server/src/filters/kb-not-found-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | import {
4 | ArgumentsHost,
5 | Catch,
6 | ExceptionFilter,
7 | NotFoundException
8 | } from '@nestjs/common';
9 |
10 | @Catch(NotFoundException)
11 | export class KbNotFoundExceptionFilter implements ExceptionFilter {
12 | constructor(private readonly appRoot: string) {}
13 |
14 | catch(exception: NotFoundException, host: ArgumentsHost) {
15 | const ctx = host.switchToHttp();
16 | const response = ctx.getResponse();
17 | const request = ctx.getRequest();
18 | const path: string = request.path;
19 |
20 | if (path.startsWith('/api/')) {
21 | response.status(exception.getStatus()).json({
22 | statusCode: exception.getStatus(),
23 | name: exception.name,
24 | error: exception.message
25 | });
26 |
27 | return;
28 | }
29 |
30 | response.sendFile(
31 | join(this.appRoot, './dist/client/index.html')
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/abstracts/base.model.abstract.spec.ts:
--------------------------------------------------------------------------------
1 | import { MockModel } from '@kb-dev-tools';
2 |
3 | describe('Base Model', () => {
4 | let model: MockModel;
5 |
6 | beforeEach(() => model = new MockModel({
7 | mockAttribute: 'nice',
8 | mockPrivateAttribute: 'bad'
9 | }));
10 |
11 | it ('should allow extending', () => {
12 | expect(model).toBeDefined();
13 | });
14 |
15 | it('should obfuscate and convert to plain object', () => {
16 | const asJson = model.toJSON();
17 |
18 | expect(model.mockAttribute).toBe('nice');
19 | expect(model.mockPrivateAttribute).toBe('bad');
20 | expect(asJson).toBeDefined();
21 | expect(asJson.mockPrivateAttribute).toBeUndefined();
22 | expect(asJson.mockAttribute).toBe('nice');
23 | });
24 |
25 | it('should obfuscate and convert to string', () => {
26 | const asString = model.toString();
27 | expect(asString).toMatchSnapshot();
28 | expect(asString).not.toMatch(/mockPrivateAttribute/g);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/server/src/app/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { mockResponse } from 'jest-mock-req-res';
2 |
3 | import { Test, TestingModule } from '@nestjs/testing';
4 |
5 | import { AppController } from '@kb-app';
6 | import { ConfigModule } from '@kb-config';
7 |
8 | describe('AppController', () => {
9 | let appController: AppController;
10 |
11 | beforeEach(async () => {
12 | const app: TestingModule = await Test.createTestingModule({
13 | imports: [ ConfigModule ],
14 | controllers: [AppController]
15 | }).compile();
16 |
17 | appController = app.get(AppController);
18 | });
19 |
20 | describe('root', () => {
21 | it('should return an HTML page', async () => {
22 | const mocRes = mockResponse();
23 | appController.sendWebClient(mocRes);
24 | expect(mocRes.sendFile.mock.calls.length).toBe(1);
25 | const param = mocRes.sendFile.mock.calls[0][0] as string;
26 | expect(param.endsWith('dist/client/index.html')).toBeTruthy();
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/achievements/src/member.achievement.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | import { IAchievement } from './achievement.abstract';
4 |
5 | export const member: IAchievement = {
6 | name: 'Member?',
7 | check: function(pullRequest, shall) {
8 | if (isWaitingLongTime(pullRequest)) {
9 |
10 | const achieve = {
11 | avatar: 'images/achievements/member.achievement.jpg',
12 | name: 'Member pull request #' + pullRequest.number + '?',
13 | short: 'Member Commits? member Push? member PR? ohh I member',
14 | description: [
15 | 'A pull request you\'ve created 2 weeks ago',
16 | ' is finally merged'
17 | ].join(''),
18 | relatedPullRequest: pullRequest.id
19 | };
20 |
21 | shall.grant(pullRequest.creator.username, achieve);
22 | }
23 | }
24 | };
25 |
26 | function isWaitingLongTime(pullRequest) {
27 | const backThen = moment(pullRequest.createdOn);
28 | const now = moment();
29 |
30 | return now.diff(backThen, 'days') > 14;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/decorators/kb-meature.decorator.ts:
--------------------------------------------------------------------------------
1 | import { WinstonLogger } from '@kibibit/nestjs-winston';
2 |
3 | const logger = new WinstonLogger('KbMeasure');
4 |
5 | export const KbMeasure = (controlerName?: string) => (
6 | target: unknown,
7 | propertyKey: string,
8 | descriptor: PropertyDescriptor
9 | ) => {
10 | const originalMethod = descriptor.value;
11 |
12 | descriptor.value = async function (...args) {
13 | logger.verbose(generateLogMessagge('START'));
14 | const start = logger.startTimer();
15 | const result = await Promise.resolve(originalMethod.apply(this, args));
16 | start.done({
17 | level: 'verbose',
18 | message: generateLogMessagge('END')
19 | });
20 | return result;
21 |
22 | function generateLogMessagge(msg: string) {
23 | return [
24 | `${ controlerName ? controlerName + '.' : '' }${ originalMethod.name }`,
25 | `(${ args && args.length ? '...' : '' }) ${ msg }`
26 | ].join('');
27 | }
28 | };
29 |
30 | return descriptor;
31 | };
32 |
--------------------------------------------------------------------------------
/achievements/src/use-github-bot.achievement.ts:
--------------------------------------------------------------------------------
1 | import { find } from 'lodash';
2 |
3 | import { IAchievement, IUserAchievement } from './achievement.abstract';
4 |
5 | export const githubBot: IAchievement = {
6 | name: 'use github bot',
7 | check: function(pullRequest, shall) {
8 | const isComitterGitHubWebFlow = find(pullRequest.commits, {
9 | committer: {
10 | username: 'web-flow'
11 | }
12 | });
13 | if (pullRequest.commits &&
14 | pullRequest.commits.length > 0 &&
15 | isComitterGitHubWebFlow) {
16 |
17 | const achieve: IUserAchievement = {
18 | avatar: 'images/achievements/useGithubBot.achievement.jpeg',
19 | name: 'Why not bots?',
20 | short: 'Hey sexy mama, wanna kill all humans?',
21 | description: [
22 | 'used github to create a pull request, using the web-flow bot'
23 | ].join(''),
24 | relatedPullRequest: pullRequest.id
25 | };
26 |
27 | shall.grant(pullRequest.creator.username, achieve);
28 | }
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/server/src/filters/kb-validation-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | BadRequestException,
4 | Catch,
5 | ExceptionFilter,
6 | HttpStatus
7 | } from '@nestjs/common';
8 |
9 | import { PublicError } from '@kb-models';
10 |
11 | @Catch(BadRequestException)
12 | export class KbValidationExceptionFilter implements ExceptionFilter {
13 | catch(exception: BadRequestException, host: ArgumentsHost) {
14 | const ctx = host.switchToHttp();
15 | const response = ctx.getResponse();
16 | const request = ctx.getRequest();
17 |
18 | response
19 | .status(HttpStatus.METHOD_NOT_ALLOWED)
20 | // you can manipulate the response here
21 | .json(new PublicError({
22 | statusCode: HttpStatus.METHOD_NOT_ALLOWED,
23 | timestamp: new Date().toISOString(),
24 | path: request.url,
25 | name: 'BadRequestException',
26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
27 | error: (exception.getResponse() as any).message as string[]
28 | }));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/achievements/src/__snapshots__/double-review.achievement.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`doubleReview achievement should be granted if 2 reviewers excluding creator 1`] = `
4 | Object {
5 | "avatar": "images/achievements/doubleReview.achievement.gif",
6 | "description": "double headed code review. It doesn't matter who added you, apparently, both of you are needed for a one man job 😇",
7 | "name": "We're ready, master",
8 | "relatedPullRequest": "test",
9 | "short": ""This way!"-"No, that way!"",
10 | }
11 | `;
12 |
13 | exports[`doubleReview achievement should be granted if 2 reviewers excluding creator 2`] = `
14 | Object {
15 | "avatar": "images/achievements/doubleReview.achievement.gif",
16 | "description": "double headed code review. It doesn't matter who added you, apparently, both of you are needed for a one man job 😇",
17 | "name": "We're ready, master",
18 | "relatedPullRequest": "test",
19 | "short": ""This way!"-"No, that way!"",
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/server/src/decorators/kb-put.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | applyDecorators,
3 | ClassSerializerInterceptor,
4 | Put,
5 | UseInterceptors
6 | } from '@nestjs/common';
7 | import {
8 | ApiBadRequestResponse,
9 | ApiNotFoundResponse,
10 | ApiOkResponse,
11 | ApiOperation
12 | } from '@nestjs/swagger';
13 |
14 | import { KbApiValidateErrorResponse } from '@kb-decorators';
15 |
16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
17 | export function KbPut(type: any, path?: string | string[]) {
18 | return applyDecorators(
19 | Put(path),
20 | ApiOperation({
21 | summary: `Update an existing ${ type.name }`,
22 | description: `Expects a full ${ type.name }`
23 | }),
24 | ApiOkResponse({ type: type, description: `${ type.name } updated` }),
25 | ApiNotFoundResponse({
26 | description: `${ type.name } not found`
27 | }),
28 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }),
29 | KbApiValidateErrorResponse(),
30 | UseInterceptors(ClassSerializerInterceptor)
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/errors/config.errors.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from 'class-validator';
2 | import { times, values } from 'lodash';
3 |
4 | export class ConfigValidationError extends Error {
5 | constructor(validationErrors: ValidationError[]) {
6 | const message = validationErrors
7 | .map((validationError) => {
8 | const productLine = ` property: ${ validationError.property } `;
9 | const valueLine = ` value: ${ validationError.value } `;
10 | const longerLineLength = Math.max(productLine.length, valueLine.length);
11 | const deco = times(longerLineLength, () => '=').join('');
12 | return [
13 | '',
14 | deco,
15 | ` property: ${ validationError.property }`,
16 | ` value: ${ validationError.value }`,
17 | deco,
18 | values(validationError.constraints)
19 | .map((value) => ` - ${ value }`).join('\n')
20 | ].join('\n');
21 | }).join('') + '\n\n';
22 |
23 | super(message);
24 | this.name = 'ConfigValidationError';
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/achievements/src/the-godfather-consigliere.achievement.ts:
--------------------------------------------------------------------------------
1 | import { result } from 'lodash';
2 |
3 | import { IAchievement, IUserAchievement } from './achievement.abstract';
4 |
5 | export const theGodfatherConsigliere: IAchievement = {
6 | name: 'The Godfather Consigliere',
7 | check: function(pullRequest, shall) {
8 | if (result(pullRequest, 'organization.username') === 'Kibibit') {
9 |
10 | const achievement: IUserAchievement = {
11 | avatar: 'images/achievements/theGodfatherConsigliere.achievement.jpg',
12 | name: 'The Godfather Consigliere',
13 | short: 'Great men are not born great, they contribute to Kibibit . . .',
14 | description: [
15 | '
You have contributed to Kibibit! We really ',
16 | 'appreciate it!
',
17 | '
Accept this achievement as gift on ',
18 | 'my daughter\'s wedding day
'
19 | ].join(''),
20 | relatedPullRequest: pullRequest.id
21 | };
22 |
23 | shall.grant(pullRequest.creator.username, achievement);
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/server/src/decorators/kb-patch.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | applyDecorators,
3 | ClassSerializerInterceptor,
4 | Patch,
5 | UseInterceptors
6 | } from '@nestjs/common';
7 | import {
8 | ApiBadRequestResponse,
9 | ApiNotFoundResponse,
10 | ApiOkResponse,
11 | ApiOperation
12 | } from '@nestjs/swagger';
13 |
14 | import { KbApiValidateErrorResponse } from '@kb-decorators';
15 |
16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
17 | export function KbPatch(type: any, path?: string | string[]) {
18 | return applyDecorators(
19 | Patch(path),
20 | ApiOperation({
21 | summary: `Update an existing ${ type.name }`,
22 | description: `Expects a partial ${ type.name }`
23 | }),
24 | ApiOkResponse({ type: type, description: `${ type.name } updated` }),
25 | ApiNotFoundResponse({
26 | description: `${ type.name } not found`
27 | }),
28 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }),
29 | KbApiValidateErrorResponse(),
30 | UseInterceptors(ClassSerializerInterceptor)
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/filters/kb-validation-exception.filter.spec.ts:
--------------------------------------------------------------------------------
1 | import MockDate from 'mockdate';
2 |
3 | import { BadRequestException, HttpStatus } from '@nestjs/common';
4 |
5 | import { hostMock, mockResponse } from '@kb-dev-tools';
6 | import { KbValidationExceptionFilter } from '@kb-filters';
7 |
8 | describe('KbValidationExceptionFilter', () => {
9 | it('should be defined', () => {
10 | expect(new KbValidationExceptionFilter()).toBeDefined();
11 | });
12 |
13 | it('should return pretty validation errors', async () => {
14 | MockDate.set('2000-05-04');
15 | const filter = new KbValidationExceptionFilter();
16 | const req = {
17 | path: '/mock-api-path',
18 | url: 'https://server.com/mock-api-path'
19 | };
20 |
21 | filter.catch(new BadRequestException(), hostMock(req, mockResponse));
22 |
23 | expect(mockResponse.status)
24 | .toHaveBeenCalledWith(HttpStatus.METHOD_NOT_ALLOWED);
25 | expect(mockResponse.json).toHaveBeenCalledTimes(1);
26 | expect(mockResponse.json.mock.calls[0][0]).toMatchSnapshot();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/achievements/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Automatically generated by barrelsby.
3 | */
4 |
5 | export * from './achievement.abstract';
6 | export * from './bi-winning.achievement';
7 | export * from './breaking-bad.achievement';
8 | export * from './cutting-edges.achievement';
9 | export * from './dont-yell-at-me.achievement';
10 | export * from './double-review.achievement';
11 | export * from './dr-claw.achievement';
12 | export * from './helping-hand.achievement';
13 | export * from './inspector-gadget.achievement';
14 | export * from './label-baby-junior.achievement';
15 | export * from './meeseek.achievement';
16 | export * from './member.achievement';
17 | export * from './mr-miyagi.achievement';
18 | export * from './never-go-full-retard.achievement';
19 | export * from './optimus-prime.achievement';
20 | export * from './reaction-on-every-comment.achievement';
21 | export * from './the-godfather-consigliere.achievement';
22 | export * from './use-github-bot.achievement';
23 | export * from './used-all-reactions-in-comment.achievement';
24 | export * from './dev-tools/mocks';
25 | export * from './dev-tools/utils';
26 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
28 |
--------------------------------------------------------------------------------
/tools/scripts/monorepo-commit-analyze.js:
--------------------------------------------------------------------------------
1 | const sgf = require('staged-git-files');
2 |
3 | const myArgs = process.argv.slice(2);
4 | const checkPart = myArgs[0];
5 |
6 | sgf((err, changedFiles) => {
7 | if (err) {
8 | throw err;
9 | }
10 |
11 | const changedFilenames = changedFiles.map((item) => item.filename);
12 | const isServerChanged = changedFilenames
13 | .find((filename) => filename.startsWith('server/'));
14 | const isClientChanged = changedFilenames
15 | .find((filename) => filename.startsWith('client/'));
16 | const isAchChanged = changedFilenames
17 | .find((filename) => filename.startsWith('achievements/'));
18 | const isToolsChanged = changedFilenames
19 | .find((filename) => filename.startsWith('tools/'));
20 |
21 | if (checkPart === 'server' && isServerChanged) {
22 | process.exit(0);
23 | }
24 |
25 | if (checkPart === 'client' && isClientChanged) {
26 | process.exit(0);
27 | }
28 |
29 | if (checkPart === 'ach' && isAchChanged) {
30 | process.exit(0);
31 | }
32 |
33 | if (checkPart === 'tools' && isToolsChanged) {
34 | process.exit(0);
35 | }
36 |
37 | process.exit(1);
38 | });
--------------------------------------------------------------------------------
/server/test/utils.ts:
--------------------------------------------------------------------------------
1 | import * as bodyParser from 'body-parser';
2 | import express from 'express';
3 |
4 | import { INestApplication } from '@nestjs/common/interfaces';
5 | import { TestingModule } from '@nestjs/testing/testing-module';
6 |
7 | import { SocketService } from './socket.service';
8 |
9 | export class Utils {
10 | public static socket: SocketService;
11 | private static server: express.Express;
12 | private static app: INestApplication;
13 | private static module: TestingModule;
14 |
15 | public static async startServer(testingModule: TestingModule) {
16 | this.module = testingModule;
17 | this.server = express();
18 | this.server.use(bodyParser.json());
19 | this.app = await testingModule.createNestApplication();
20 | await this.app.init();
21 | }
22 |
23 | public static async createSocket(defer = false) {
24 | await this.app.listen(10109);
25 | this.socket = new SocketService(defer);
26 |
27 | return this.socket;
28 | }
29 | public static async closeApp() {
30 | this.socket.close();
31 | await this.app.close();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 kibibit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "esModuleInterop": true,
12 | "outDir": "../dist/server",
13 | "baseUrl": "./",
14 | "paths": {
15 | "@kb-models": [ "src/models/index" ],
16 | "@kb-abstracts": [ "src/abstracts/index" ],
17 | "@kb-decorators": [ "src/decorators/index" ],
18 | "@kb-filters": [ "src/filters/index" ],
19 | "@kb-events": [ "src/events/index" ],
20 | "@kb-tasks": [ "src/tasks/index" ],
21 | "@kb-app": [ "src/app/index" ],
22 | "@kb-api": [ "src/api/index" ],
23 | "@kb-dev-tools": [ "src/dev-tools/index" ],
24 | "@kb-interfaces": [ "src/interfaces/index" ],
25 | "@kb-engines": [ "src/engines/index" ],
26 | "@kb-errors": [ "src/errors/index" ],
27 | "@kb-config": [ "src/config/index" ]
28 |
29 | },
30 | "incremental": true,
31 | "skipLibCheck": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: [
13 | './src/**/*.e2e-spec.ts'
14 | ],
15 | capabilities: {
16 | browserName: 'chrome',
17 | chromeOptions: {
18 | args: [ '--headless', '--disable-gpu', '--window-size=800,600', '--log-level=3', '--no-sandbox' ]
19 | }
20 | },
21 | directConnect: true,
22 | baseUrl: 'http://localhost:10101/',
23 | framework: 'jasmine',
24 | jasmineNodeOpts: {
25 | showColors: true,
26 | defaultTimeoutInterval: 30000,
27 | print: function() {}
28 | },
29 | onPrepare() {
30 | require('ts-node').register({
31 | project: require('path').join(__dirname, './tsconfig.json')
32 | });
33 | jasmine.getEnv().addReporter(new SpecReporter({
34 | spec: {
35 | displayStacktrace: StacktraceOption.PRETTY
36 | }
37 | }));
38 | }
39 | };
--------------------------------------------------------------------------------
/achievements/src/use-github-bot.achievement.spec.ts:
--------------------------------------------------------------------------------
1 | import { PullRequest, Shall } from './dev-tools/mocks';
2 | import { githubBot } from './use-github-bot.achievement';
3 |
4 | const mockCommits = [
5 | {
6 | author: {
7 | username: 'commit-author'
8 | },
9 | committer: {
10 | username: 'web-flow'
11 | }
12 | }
13 | ];
14 |
15 | describe('githubBot achievement', () => {
16 | it('should be granted if committer username is web-flow', () => {
17 | const testShall = new Shall();
18 | const pullRequest = new PullRequest();
19 | pullRequest.commits = mockCommits;
20 |
21 | githubBot.check(pullRequest, testShall);
22 | expect(testShall.grantedAchievements).toBeDefined();
23 | expect(testShall.grantedAchievements.creator).toMatchSnapshot();
24 | });
25 |
26 | it('should not grant if committer is not web-flow', () => {
27 | const testShall = new Shall();
28 | const pullRequest = new PullRequest();
29 | pullRequest.commits = mockCommits;
30 | pullRequest.commits[0].committer.username = 'not-web-flow';
31 |
32 | githubBot.check(pullRequest, testShall);
33 | expect(testShall.grantedAchievements).toBeUndefined();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/achievements/src/double-review.achievement.ts:
--------------------------------------------------------------------------------
1 | import { clone, escape, forEach, remove } from 'lodash';
2 |
3 | import { IAchievement, IUserAchievement } from './achievement.abstract';
4 |
5 | export const doubleReview: IAchievement = {
6 | name: 'doubleReview',
7 | check: function(pullRequest, shall) {
8 | // clone the reviewers to not mutate the original pullRequest
9 | const reviewers = clone(pullRequest.reviewers);
10 | remove(reviewers, {
11 | username: pullRequest.creator.username
12 | });
13 | if (reviewers && reviewers.length === 2) {
14 |
15 | const achieve: IUserAchievement = {
16 | avatar: 'images/achievements/doubleReview.achievement.gif',
17 | name: 'We\'re ready, master',
18 | short: escape('"This way!"-"No, that way!"'),
19 | description: [
20 | 'double headed code review. It doesn\'t matter who added you, ',
21 | 'apparently, both of you are needed for a one man job 😇'
22 | ].join(''),
23 | relatedPullRequest: pullRequest.id
24 | };
25 |
26 | forEach(reviewers, function(reviewer) {
27 | shall.grant(reviewer.username, achieve);
28 | });
29 | }
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/server/src/api/pull-request/pull-request.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, Type } from '@nestjs/common';
2 | import { MongooseModule } from '@nestjs/mongoose';
3 |
4 | import { WinstonLogger } from '@kibibit/nestjs-winston';
5 |
6 | import { ConfigService } from '@kb-config';
7 | import { PullRequest } from '@kb-models';
8 |
9 | import { PullRequestController } from './pull-request.controller';
10 | import { PullRequestService } from './pull-request.service';
11 |
12 | const devControllers: Type[] = [PullRequestController];
13 | const logger = new WinstonLogger('PullRequestModule');
14 |
15 | const config = new ConfigService();
16 | const controllers = (() => {
17 | if (config.nodeEnv === 'production') {
18 | return [];
19 | } else {
20 | logger.log('Not running in production mode!');
21 | logger.debug('Attaching Pull Request controller for development');
22 | return devControllers;
23 | }
24 | })();
25 |
26 | @Module({
27 | imports: [
28 | MongooseModule.forFeature([
29 | { name: PullRequest.modelName, schema: PullRequest.schema }
30 | ])
31 | ],
32 | providers: [PullRequestService],
33 | controllers,
34 | exports: [PullRequestService]
35 | })
36 | export class PullRequestModule {}
37 |
--------------------------------------------------------------------------------
/achievements/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/eslint-recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | ],
12 | ignorePatterns: [ '.eslintrc.js' ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | rules: {
19 | 'eol-last': [ 2, 'windows' ],
20 | 'comma-dangle': [ 'error', 'never' ],
21 | 'max-len': [ 'error', { 'code': 80, "ignoreComments": true } ],
22 | 'quotes': ["error", "single"],
23 | '@typescript-eslint/no-empty-interface': 'error',
24 | '@typescript-eslint/member-delimiter-style': 'error',
25 | '@typescript-eslint/explicit-function-return-type': 'off',
26 | '@typescript-eslint/explicit-module-boundary-types': 'off',
27 | '@typescript-eslint/naming-convention': [
28 | "error",
29 | {
30 | "selector": "interface",
31 | "format": ["PascalCase"],
32 | "custom": {
33 | "regex": "^I[A-Z]",
34 | "match": true
35 | }
36 | }
37 | ]
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/server/src/api/webhook-event-manager/webhook-event-manager.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Headers,
5 | Logger,
6 | Post,
7 | UseFilters
8 | } from '@nestjs/common';
9 | import { ApiOperation, ApiTags } from '@nestjs/swagger';
10 |
11 | import { KbValidationExceptionFilter } from '@kb-filters';
12 | import { IGithubPullRequestEvent } from '@kb-interfaces';
13 |
14 | import { WebhookEventManagerService } from './webhook-event-manager.service';
15 |
16 | @Controller('api/webhook-event-manager')
17 | @ApiTags('Webhook Event Manager')
18 | @UseFilters(new KbValidationExceptionFilter())
19 | export class WebhookEventManagerController {
20 | private readonly logger = new Logger(WebhookEventManagerController.name);
21 |
22 | constructor(
23 | private readonly webhookEventManagerService: WebhookEventManagerService
24 | ) {}
25 |
26 | @Post()
27 | @ApiOperation({ summary: 'Recieve GitHub Webhooks' })
28 | async recieveGitHubWebhooks(
29 | @Headers('x-github-event') githubEvent: string,
30 | @Body() eventBody: IGithubPullRequestEvent
31 | ): Promise {
32 | const eventName = await this.webhookEventManagerService
33 | .notifyAchievements(githubEvent, eventBody);
34 |
35 | return eventName;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/src/models/repo.model.ts:
--------------------------------------------------------------------------------
1 | import { Exclude, Expose } from 'class-transformer';
2 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
3 | import { index, modelOptions, prop as PersistInDb } from '@typegoose/typegoose';
4 |
5 | import { ApiProperty } from '@nestjs/swagger';
6 |
7 | import { BaseModel } from '@kb-abstracts';
8 |
9 | @Exclude()
10 | @modelOptions({
11 | schemaOptions: {
12 | collation: { locale: 'en_US', strength: 2 },
13 | timestamps: true
14 | }
15 | })
16 | @index({ fullname: 1 }, { unique: true })
17 | export class Repo extends BaseModel {
18 |
19 | @Expose()
20 | @ApiProperty()
21 | @IsNotEmpty()
22 | @PersistInDb({ required: true })
23 | readonly name: string;
24 |
25 | @Expose()
26 | @ApiProperty()
27 | @IsNotEmpty()
28 | @PersistInDb({ required: true, unique: true })
29 | readonly fullname: string;
30 |
31 | @Expose()
32 | @ApiProperty()
33 | @IsString()
34 | @PersistInDb({ required: true })
35 | readonly url: string;
36 |
37 | @Expose()
38 | @ApiProperty()
39 | @IsString()
40 | @IsOptional()
41 | @PersistInDb()
42 | readonly organization: string;
43 |
44 | constructor(partial: Partial = {}) {
45 | super();
46 | Object.assign(this, partial);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/achievements/src/bi-winning.achievement.ts:
--------------------------------------------------------------------------------
1 | import { every, isEmpty } from 'lodash';
2 |
3 | import { IAchievement, IUserAchievement } from './achievement.abstract';
4 |
5 | export const biWinning: IAchievement = {
6 | name: 'bi-winning',
7 | check: function(pullRequest, shall) {
8 | if (!isEmpty(pullRequest.commits) &&
9 | every(pullRequest.commits, allStatusesPassed)) {
10 |
11 | const achievement: IUserAchievement = {
12 | avatar: 'images/achievements/biWinning.achievement.jpg',
13 | name: 'BI-WINNING!',
14 | short: 'I\'m bi-winning. I win here and I win there',
15 | description: [
16 | '
All the commits in your pull-request have passing statuses! ',
17 | 'WINNING!
',
18 | '
I\'m different. I have a different constitution, I have a ',
19 | 'different brain, I have a different heart. I got tiger blood, man. ',
20 | 'Dying\'s for fools, dying\'s for amateurs.