├── .all-contributorsrc ├── .doc └── img │ ├── architecture_and_data_flow.png │ ├── step1.png │ ├── step2.png │ └── step3.png ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── enhancement_request.md │ └── feature_request.md ├── actions │ ├── backend_test │ │ └── action.yaml │ └── frontend_test │ │ └── action.yaml └── workflows │ ├── backend_test.yaml │ ├── build_release_docker_image.yaml │ └── frontend_test.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README-CH.md ├── README.md ├── backend ├── .gitignore ├── README.md ├── build.gradle.kts ├── connect-to-mongodb.sh ├── encryption │ └── EncryptionHelper.kts ├── gradle │ ├── detekt │ │ └── detekt.yml │ ├── git-hooks │ │ ├── install-git-hooks.gradle │ │ └── pre-push │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mongodb-setup │ ├── config │ │ ├── add-user.js │ │ ├── init.sh │ │ ├── keyfile.txt │ │ ├── replica-set-init.js │ │ └── startup.sh │ └── mongodb-for-local │ │ ├── docker-compose-for-local.yml │ │ ├── remove-mongodb.sh │ │ └── setup-mongodb.sh ├── run.sh ├── scripts │ ├── backend-service-check.sh │ ├── install-mongo-from-tarball.sh │ ├── mongodb-service-check.sh │ └── tcp-port-check.sh ├── settings.gradle.kts └── src │ ├── apiTest │ ├── kotlin │ │ └── metrik │ │ │ ├── CFRCalculationApiTest.kt │ │ │ ├── DFCalculationApiTest.kt │ │ │ ├── MLTCalculationApiTest.kt │ │ │ ├── MTTRCalculationApiTest.kt │ │ │ ├── MultiPipelineCalculationApiTest.kt │ │ │ ├── PipelineApiTest.kt │ │ │ ├── ProjectApiTest.kt │ │ │ ├── base │ │ │ └── ApiTestBase.kt │ │ │ ├── config │ │ │ └── ApiTestConfiguration.kt │ │ │ └── fixtures │ │ │ ├── ChangeFailureRateFixtureData.kt │ │ │ ├── DeploymentFrequencyFixtureData.kt │ │ │ ├── MeanLeadTimeFixtureData.kt │ │ │ ├── MeanTimeToRestoreFixtureData.kt │ │ │ └── MultiPipelineFixtureData.kt │ └── resources │ │ └── application-apitest.yml │ ├── main │ ├── kotlin │ │ └── metrik │ │ │ ├── Application.kt │ │ │ ├── configuration │ │ │ ├── CacheConfiguration.kt │ │ │ ├── MongoConfiguration.kt │ │ │ ├── RestTemplateConfiguration.kt │ │ │ └── SwaggerUIConfiguration.kt │ │ │ ├── exception │ │ │ ├── ApplicationException.kt │ │ │ ├── ErrorResponse.kt │ │ │ └── GlobalExceptionHandler.kt │ │ │ ├── infrastructure │ │ │ ├── encryption │ │ │ │ └── AESEncryption.kt │ │ │ ├── serializer │ │ │ │ └── NumberSerializer.kt │ │ │ └── utlils │ │ │ │ ├── RequestUtil.kt │ │ │ │ └── TimeFormatExtensions.kt │ │ │ ├── metrics │ │ │ ├── domain │ │ │ │ ├── calculator │ │ │ │ │ ├── ChangeFailureRateCalculator.kt │ │ │ │ │ ├── DeploymentFrequencyCalculator.kt │ │ │ │ │ ├── LeadTimeForChangeCalculator.kt │ │ │ │ │ ├── MeanTimeToRestoreCalculator.kt │ │ │ │ │ └── MetricsCalculator.kt │ │ │ │ └── model │ │ │ │ │ ├── CalculationPeriod.kt │ │ │ │ │ └── Metrics.kt │ │ │ ├── exception │ │ │ │ └── BadRequestException.kt │ │ │ └── rest │ │ │ │ ├── MetricsApplicationService.kt │ │ │ │ ├── MetricsController.kt │ │ │ │ ├── TimeRangeSplitter.kt │ │ │ │ └── vo │ │ │ │ ├── FourKeyMetricsRequest.kt │ │ │ │ └── FourKeyMetricsResponse.kt │ │ │ └── project │ │ │ ├── constant │ │ │ └── GithubActionConstants.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ ├── Execution.kt │ │ │ │ ├── PipelineConfiguration.kt │ │ │ │ ├── PipelineType.kt │ │ │ │ └── Project.kt │ │ │ ├── repository │ │ │ │ ├── BuildRepository.kt │ │ │ │ ├── CommitRepository.kt │ │ │ │ ├── PipelineRepository.kt │ │ │ │ └── ProjectRepository.kt │ │ │ └── service │ │ │ │ ├── PipelineService.kt │ │ │ │ ├── bamboo │ │ │ │ ├── BambooDTO.kt │ │ │ │ ├── BambooDeploymentPipelineService.kt │ │ │ │ └── BambooPipelineService.kt │ │ │ │ ├── buddy │ │ │ │ ├── BuddyDTO.kt │ │ │ │ └── BuddyPipelineService.kt │ │ │ │ ├── factory │ │ │ │ ├── NoopPipelineService.kt │ │ │ │ └── PipelineServiceFactory.kt │ │ │ │ ├── githubactions │ │ │ │ ├── BranchService.kt │ │ │ │ ├── CommitService.kt │ │ │ │ ├── ExecutionConverter.kt │ │ │ │ ├── GithubActionsPipelineService.kt │ │ │ │ ├── GithubCommit.kt │ │ │ │ ├── PipelineCommitService.kt │ │ │ │ ├── PipelineRunService.kt │ │ │ │ ├── Run.kt │ │ │ │ └── RunService.kt │ │ │ │ └── jenkins │ │ │ │ ├── JenkinsDTO.kt │ │ │ │ └── JenkinsPipelineService.kt │ │ │ ├── exception │ │ │ ├── PipelineConfigVerifyException.kt │ │ │ ├── PipelineNotFoundException.kt │ │ │ ├── ProjectNameDuplicateException.kt │ │ │ ├── ProjectNotFoundException.kt │ │ │ └── SynchronizationException.kt │ │ │ ├── infrastructure │ │ │ ├── bamboo │ │ │ │ └── feign │ │ │ │ │ └── BambooFeignClient.kt │ │ │ ├── buddy │ │ │ │ └── feign │ │ │ │ │ └── BuddyFeignClient.kt │ │ │ ├── github │ │ │ │ └── feign │ │ │ │ │ ├── GithubFeignClient.kt │ │ │ │ │ └── response │ │ │ │ │ ├── BranchResponse.kt │ │ │ │ │ ├── CommitResponse.kt │ │ │ │ │ ├── MultipleRunResponse.kt │ │ │ │ │ └── SingleRunResponse.kt │ │ │ └── jenkins │ │ │ │ └── feign │ │ │ │ └── JenkinsFeignClient.kt │ │ │ └── rest │ │ │ ├── PipelineController.kt │ │ │ ├── ProjectController.kt │ │ │ ├── SynchronizationController.kt │ │ │ ├── applicationservice │ │ │ ├── PipelineApplicationService.kt │ │ │ ├── ProjectApplicationService.kt │ │ │ └── SynchronizationApplicationService.kt │ │ │ ├── validation │ │ │ ├── EnumConstraint.kt │ │ │ └── EnumValidator.kt │ │ │ └── vo │ │ │ ├── request │ │ │ ├── BambooDeploymentRequest.kt │ │ │ ├── BambooRequest.kt │ │ │ ├── BuddyRequest.kt │ │ │ ├── GithubActionsRequest.kt │ │ │ ├── JenkinsRequest.kt │ │ │ └── Request.kt │ │ │ └── response │ │ │ ├── PipelineResponse.kt │ │ │ ├── PipelineStagesResponse.kt │ │ │ ├── ProjectDetailResponse.kt │ │ │ ├── ProjectResponse.kt │ │ │ └── SyncProgress.kt │ └── resources │ │ ├── application-local.yml │ │ ├── application-release.yml │ │ ├── application.yml │ │ ├── log4j2-console.xml │ │ └── log4j2-file-and-console.xml │ └── test │ ├── kotlin │ └── metrik │ │ ├── exception │ │ └── GlobalExceptionHandlerTest.kt │ │ ├── infrastructure │ │ ├── encryption │ │ │ ├── AESEncryptionServiceTest.kt │ │ │ └── DatabaseEncryptionAspectTest.kt │ │ ├── serializer │ │ │ └── NumberSerializerTest.kt │ │ └── utlils │ │ │ ├── RequestUtilTest.kt │ │ │ └── TimeFormatUtilTest.kt │ │ ├── metrics │ │ ├── domain │ │ │ └── calculator │ │ │ │ ├── ChangeFailureRateCalculatorTest.kt │ │ │ │ ├── DeploymentFrequencyCalculatorTest.kt │ │ │ │ ├── LeadTimeForChangeCalculatorTest.kt │ │ │ │ └── MeanTimeToRestoreCalculatorTest.kt │ │ └── rest │ │ │ ├── MetricsApplicationServiceTest.kt │ │ │ ├── MetricsControllerTest.kt │ │ │ └── TimeRangeSplitterKtTest.kt │ │ └── project │ │ ├── TestFixture.kt │ │ ├── domain │ │ ├── model │ │ │ └── StageTest.kt │ │ ├── repository │ │ │ ├── BuildRepositoryTest.kt │ │ │ ├── CommitRepositoryTest.kt │ │ │ ├── PipelineRepositoryTest.kt │ │ │ └── ProjectRepositoryTest.kt │ │ └── service │ │ │ ├── bamboo │ │ │ ├── BambooDeploymentPipelineServiceTest.kt │ │ │ └── BambooPipelineServiceTest.kt │ │ │ ├── buddy │ │ │ └── BuddyPipelineServiceTest.kt │ │ │ ├── factory │ │ │ └── GithubPipelineServiceFactoryTest.kt │ │ │ ├── githubactions │ │ │ ├── BranchServiceTest.kt │ │ │ ├── CommitServiceTest.kt │ │ │ ├── ExecutionConverterTest.kt │ │ │ ├── GithubActionsPipelineServiceTest.kt │ │ │ ├── PipelineCommitServiceTest.kt │ │ │ ├── PipelineRunServiceTest.kt │ │ │ └── RunServiceTest.kt │ │ │ └── jenkins │ │ │ └── JenkinsPipelineServiceTest.kt │ │ ├── infrastructure │ │ └── bamboo │ │ │ └── feign │ │ │ └── BambooFeignClientTest.kt │ │ └── rest │ │ ├── PipelineControllerTest.kt │ │ ├── ProjectControllerTest.kt │ │ ├── SynchronizationControllerTest.kt │ │ ├── applicationservice │ │ ├── PipelineApplicationServiceTest.kt │ │ ├── ProjectApplicationServiceTest.kt │ │ └── SynchronizationApplicationServiceTest.kt │ │ └── vo │ │ └── request │ │ └── GithubActionsPipelineRequestTest.kt │ └── resources │ ├── application.yml │ ├── calculator │ ├── builds-for-CFR-case-1.json │ ├── builds-for-CFR-case-2.json │ ├── builds-for-DF-case-1.json │ ├── builds-for-DF-case-3.json │ ├── builds-for-MLT-case-1.json │ ├── builds-for-MLT-case-10.json │ ├── builds-for-MLT-case-2.json │ ├── builds-for-MLT-case-3.json │ ├── builds-for-MLT-case-4.json │ ├── builds-for-MLT-case-5-1.json │ ├── builds-for-MLT-case-5-2.json │ ├── builds-for-MLT-case-5-3.json │ ├── builds-for-MLT-case-5.json │ ├── builds-for-MLT-case-6.json │ ├── builds-for-MLT-case-7.json │ ├── builds-for-MLT-case-8.json │ ├── builds-for-MLT-case-9.json │ ├── builds-for-MTTR-case-1.json │ ├── builds-for-MTTR-case-2.json │ ├── builds-for-MTTR-case-3.json │ ├── builds-for-MTTR-case-4.json │ ├── builds-for-MTTR-case-5.json │ ├── builds-for-MTTR-case-6.json │ └── builds-for-MTTR-case-7.json │ ├── pipeline │ ├── bamboo │ │ ├── raw-build-details-1.json │ │ ├── raw-build-details-2.json │ │ ├── raw-build-details-3.json │ │ ├── raw-build-details-4.json │ │ ├── raw-build-details-5.json │ │ ├── raw-build-details-6.json │ │ ├── raw-build-details-7.json │ │ ├── raw-build-details-8.json │ │ ├── raw-build-summary-1.json │ │ ├── raw-build-summary-2.json │ │ ├── raw-build-summary-3.json │ │ ├── raw-build-summary-4.json │ │ ├── raw-build-summary-5.json │ │ ├── raw-build-summary-6.json │ │ ├── raw-build-summary-7.json │ │ ├── raw-build-summary-8.json │ │ ├── raw-deploy-build-details-1.json │ │ ├── raw-deploy-build-details-2.json │ │ ├── raw-deploy-build-details-3.json │ │ ├── raw-deploy-build-details-4.json │ │ ├── raw-deploy-build-details-5.json │ │ ├── raw-deploy-build-summary.json │ │ ├── raw-deploy-project-summary.json │ │ ├── raw-deploy-results-1.json │ │ ├── raw-deploy-results-2.json │ │ ├── raw-deploy-version-1.json │ │ └── raw-deploy-version-2.json │ ├── buddy │ │ ├── executions-empty-unpaged.json │ │ ├── executions-empty.json │ │ ├── executions-page-1.json │ │ ├── executions-page-2.json │ │ ├── executions-unpaged.json │ │ ├── verify-pipeline.json │ │ └── verify-project.json │ ├── githubactions │ │ ├── commits │ │ │ ├── commit1.json │ │ │ └── empty-commit.json │ │ ├── run │ │ │ ├── in-progress-update-runs1.json │ │ │ └── in-progress-update-runs2.json │ │ ├── runs │ │ │ ├── empty-run.json │ │ │ ├── in-progress-runs1.json │ │ │ ├── non-supported-runs1.json │ │ │ ├── runs1.json │ │ │ ├── runs2.json │ │ │ └── runs3.json │ │ └── verify-pipeline │ │ │ ├── runs-verify1.json │ │ │ └── runs-verify2.json │ └── jenkins │ │ ├── expected │ │ ├── builds-for-jenkins-1.json │ │ ├── builds-for-jenkins-2.json │ │ ├── builds-for-jenkins-3.json │ │ ├── builds-for-jenkins-4.json │ │ └── builds-for-jenkins-5.json │ │ ├── raw-build-detail-1.json │ │ ├── raw-build-detail-2.json │ │ ├── raw-build-detail-3.json │ │ ├── raw-build-detail-4.json │ │ ├── raw-build-detail-5.json │ │ ├── raw-build-summary-1.json │ │ ├── raw-build-summary-2.json │ │ ├── raw-build-summary-3.json │ │ ├── raw-build-summary-4.json │ │ └── raw-build-summary-5.json │ └── repository │ ├── builds-for-build-repo-1.json │ ├── builds-for-build-repo-2.json │ ├── builds-for-build-repo-3.json │ ├── builds-for-pipeline-id-1.json │ ├── dashboards-1.json │ ├── dashboards-2.json │ ├── dashboards-with-pipelines.json │ ├── dashboards-without-pipelines.json │ └── pipelines-in-one-dashboard.json ├── ci ├── .gitignore ├── Dockerfile ├── README.md └── config │ ├── mongo │ ├── mongo-create-user.js │ ├── mongo-init-replica-set.js │ └── mongo-init.sh │ ├── nginx_release.conf │ └── supervisord.conf └── frontend ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintignore ├── .stylelintrc ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public └── index.html ├── scripts ├── constants.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── src ├── App.tsx ├── assets │ ├── fonts │ │ ├── Oswald │ │ │ ├── OFL.txt │ │ │ ├── Oswald-VariableFont_wght.ttf │ │ │ ├── README.txt │ │ │ └── static │ │ │ │ ├── Oswald-Bold.ttf │ │ │ │ ├── Oswald-ExtraLight.ttf │ │ │ │ ├── Oswald-Light.ttf │ │ │ │ ├── Oswald-Medium.ttf │ │ │ │ ├── Oswald-Regular.ttf │ │ │ │ └── Oswald-SemiBold.ttf │ │ └── fonts.less │ ├── icons │ │ ├── OldLogo.tsx │ │ └── Title.tsx │ └── source │ │ ├── favicon.svg │ │ ├── logo.svg │ │ └── title.svg ├── clients │ ├── createRequest.ts │ ├── metricsApis.ts │ ├── pipelineApis.ts │ └── projectApis.ts ├── components │ ├── AreaChart │ │ └── AreaChart.tsx │ ├── ColourLegend.tsx │ ├── EditableText.tsx │ ├── Favicon │ │ └── Favicon.tsx │ ├── Header.tsx │ ├── HintIcon.tsx │ ├── LegendRect.tsx │ ├── LineChart.tsx │ ├── LoadingSpinner.tsx │ ├── Logo │ │ └── Logo.tsx │ ├── MultipleCascadeSelect.tsx │ ├── PipelineSetup │ │ └── PipelineSetup.tsx │ ├── ProjectConfig.tsx │ └── Word │ │ └── Word.tsx ├── constants │ ├── date-format.ts │ ├── errorMessages.tsx │ ├── metrics.ts │ ├── styles.ts │ └── tooltips.ts ├── globalStyle.ts ├── hooks │ ├── useModalVisible.ts │ ├── usePipelineSetting.ts │ ├── usePrevious.ts │ ├── useQuery.ts │ └── useRequest.ts ├── models │ ├── common.ts │ ├── metrics.ts │ └── pipeline.ts ├── pages │ ├── config │ │ ├── PageConfig.tsx │ │ └── components │ │ │ ├── ConfigSuccess.tsx │ │ │ └── ProjectNameSetup.tsx │ └── dashboard │ │ ├── PageDashboard.tsx │ │ ├── components │ │ ├── DashboardTopPanel.tsx │ │ ├── Fullscreen │ │ │ ├── Fullscreen.tsx │ │ │ └── components │ │ │ │ ├── FullscreenDashboard.tsx │ │ │ │ ├── FullscreenMetricsCard.tsx │ │ │ │ ├── MetricsLegend.tsx │ │ │ │ └── PipelineList.tsx │ │ ├── MetricInfo.tsx │ │ ├── MetricTooltip.tsx │ │ ├── MetricsCard.tsx │ │ ├── PipelineSetting.tsx │ │ └── SyncProgressContent.tsx │ │ ├── context │ │ └── DashboardContext.tsx │ │ └── utils │ │ ├── fullScreenDataProcess.test.ts │ │ └── fullScreenDataProcess.ts ├── routes │ └── Routes.tsx └── utils │ ├── calcMaxValueWithRatio │ ├── calcMaxValueWithRatio.test.ts │ └── calcMaxValueWithRatio.ts │ ├── dataTransform │ ├── dataTransform.test.ts │ └── dataTransform.ts │ ├── metricsDataUtils │ ├── metricsDataUtils.test.ts │ └── metricsDataUtils.ts │ ├── pipelineConfig │ ├── bambooConfig.tsx │ ├── bambooDeployedConfig.tsx │ ├── buddyConfig.tsx │ ├── githubActionsConfig.tsx │ ├── jenkinsConfig.tsx │ └── pipelineConfig.tsx │ ├── responsive │ ├── responsive.test.ts │ └── responsive.ts │ ├── timeFormats │ ├── timeFormats.test.ts │ └── timeFormats.ts │ └── validates │ ├── validates.test.ts │ └── validates.ts ├── test ├── jsdomHelper.ts ├── mocks │ ├── fileMock.ts │ └── styleMock.ts └── setup.ts └── tsconfig.json /.doc/img/architecture_and_data_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/.doc/img/architecture_and_data_flow.png -------------------------------------------------------------------------------- /.doc/img/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/.doc/img/step1.png -------------------------------------------------------------------------------- /.doc/img/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/.doc/img/step2.png -------------------------------------------------------------------------------- /.doc/img/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/.doc/img/step3.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement request 3 | about: Suggest an enhancement for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the context of this enhancement** 11 | A clear and concise description of what the context is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the enhancement request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/backend_test/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Backend API test' 2 | description: 'Run backend service API tests with MongoDB' 3 | 4 | inputs: 5 | working-dir: 6 | description: 'The path of backend module' 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Grant execute permission for shell scripts 13 | run: | 14 | chmod +x gradlew 15 | chmod +x ./scripts/*.sh 16 | shell: bash 17 | working-directory: ${{ inputs.working-dir }} 18 | 19 | - name: Run Gradle build task, including unit test, integration test, and lint check 20 | run: ./gradlew clean build 21 | shell: bash 22 | working-directory: ${{ inputs.working-dir }} 23 | -------------------------------------------------------------------------------- /.github/actions/frontend_test/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Frontend module test' 2 | description: 'Run frontend module tests' 3 | 4 | inputs: 5 | working-dir: 6 | description: 'The path of frontend module' 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Install dependencies 13 | run: npm ci 14 | shell: bash 15 | working-directory: ${{ inputs.working-dir }} 16 | 17 | - name: Run tests 18 | run: npm run test 19 | shell: bash 20 | working-directory: ${{ inputs.working-dir }} 21 | 22 | - name: Build frontend artifacts 23 | run: npm run build:prod 24 | shell: bash 25 | working-directory: ${{ inputs.working-dir }} 26 | -------------------------------------------------------------------------------- /.github/workflows/backend_test.yaml: -------------------------------------------------------------------------------- 1 | name: Backend test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - backend/** 9 | - .github/actions/backend_test/** 10 | - .github/workflows/backend_test.yaml 11 | 12 | pull_request: 13 | branches: 14 | - main 15 | paths: 16 | - backend/** 17 | - .github/actions/backend_test/** 18 | - .github/workflows/backend_test.yaml 19 | 20 | workflow_dispatch: 21 | 22 | jobs: 23 | backend-test: 24 | name: Backend module test 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Setup timezone 29 | run: sudo timedatectl set-timezone "Asia/Shanghai" 30 | 31 | - uses: actions/checkout@v2 32 | 33 | - name: Setup Java JDK 34 | uses: actions/setup-java@v2 35 | with: 36 | distribution: 'adopt' 37 | java-package: 'jdk' 38 | java-version: '11' 39 | check-latest: true 40 | 41 | - name: Cache Gradle packages 42 | uses: actions/cache@v2 43 | with: 44 | path: | 45 | ~/.gradle/caches 46 | ~/.gradle/wrapper 47 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 48 | restore-keys: | 49 | ${{ runner.os }}-gradle- 50 | 51 | - name: Run backend tests 52 | uses: ./.github/actions/backend_test 53 | with: 54 | working-dir: ./backend 55 | 56 | - name: Upload Kover coverage reports 57 | uses: codecov/codecov-action@v2 58 | with: 59 | files: ./backend/build/reports/kover/report.xml,./backend/build/reports/kover/html/index.html 60 | 61 | ## This step allow you to debug via SSH 62 | # - name: Setup tmate session 63 | # uses: mxschmitt/action-tmate@v3 64 | # with: 65 | # sudo: true 66 | -------------------------------------------------------------------------------- /.github/workflows/frontend_test.yaml: -------------------------------------------------------------------------------- 1 | name: Frontend test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - frontend/** 9 | - .github/actions/frontend_test/** 10 | - .github/workflows/frontend_test.yaml 11 | 12 | pull_request: 13 | branches: 14 | - main 15 | paths: 16 | - frontend/** 17 | - .github/actions/frontend_test/** 18 | - .github/workflows/frontend_test.yaml 19 | 20 | workflow_dispatch: 21 | 22 | jobs: 23 | frontend-test: 24 | name: Frontend module test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Setup timezone 28 | run: sudo timedatectl set-timezone "Asia/Shanghai" 29 | 30 | - uses: actions/checkout@v2 31 | 32 | - name: Setup Node.js 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: '14' 36 | 37 | - name: Cache Node.js packages 38 | uses: actions/cache@v2 39 | with: 40 | path: ~/.npm 41 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 42 | restore-keys: | 43 | ${{ runner.os }}-node- 44 | 45 | - name: Run frontend tests 46 | uses: ./.github/actions/frontend_test 47 | with: 48 | working-dir: ./frontend 49 | 50 | ## This step allow you to debug via SSH 51 | # - name: Setup tmate session 52 | # uses: mxschmitt/action-tmate@v3 53 | # with: 54 | # sudo: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea 3 | .ideaDataSources 4 | *.iws 5 | *.iml 6 | *.ipr 7 | 8 | ### VS Code ### 9 | .vscode/ 10 | 11 | ### macOS Temporary Files ### 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Othneil Drew 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 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | logs/ 7 | mongodb-setup/mongodb-for-apitest/mongo-db-data/ 8 | mongodb-setup/mongodb-for-local/mongo-db-data/ 9 | 10 | ### STS ### 11 | .apt_generated 12 | .classpath 13 | .factorypath 14 | .project 15 | .settings 16 | .springBeans 17 | .sts4-cache 18 | bin/ 19 | !**/src/main/**/bin/ 20 | !**/src/test/**/bin/ 21 | 22 | ### IntelliJ IDEA ### 23 | .idea 24 | .ideaDataSources 25 | *.iws 26 | *.iml 27 | *.ipr 28 | out/ 29 | !**/src/main/**/out/ 30 | !**/src/test/**/out/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### macOS Temporary Files ### 43 | .DS_Store 44 | -------------------------------------------------------------------------------- /backend/connect-to-mongodb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | container_id=$(docker ps --filter="name=mongo" -q) 4 | 5 | for file in `find $PWD/src/api-test/resources/ -type f -name '*.js'` 6 | do 7 | docker exec -i $container_id mongo --port 27017 -u "4km" --authenticationDatabase "4-key-metrics" -p "4000km" 4-key-metrics < $file 8 | done -------------------------------------------------------------------------------- /backend/encryption/EncryptionHelper.kts: -------------------------------------------------------------------------------- 1 | package fourkeymetrics.common.encryption 2 | 3 | import java.util.* 4 | import javax.crypto.Cipher 5 | import javax.crypto.SecretKey 6 | import javax.crypto.spec.IvParameterSpec 7 | import javax.crypto.spec.SecretKeySpec 8 | 9 | 10 | class AESEncryptionHelper { 11 | private var keyString: String? 12 | private var ivString: String? 13 | private var key: SecretKey 14 | private var iv: IvParameterSpec 15 | private val algorithm = "AES/CBC/PKCS5Padding" 16 | 17 | init { 18 | print("Please enter key, you can find it for every env in src/main/resources, or press ENTER to use default value for local:") 19 | keyString = readLine() 20 | if (keyString == "") { 21 | keyString = "&E)H@MbQeThWmZq4" 22 | } 23 | println("Key: $keyString") 24 | print("Please enter IV, you can find it for every env in src/main/resources, or press ENTER to use default value for local:") 25 | ivString = readLine() 26 | if (ivString == "") { 27 | ivString = "D(G+KbPeShVkYp3s" 28 | } 29 | println("IV: $ivString") 30 | 31 | this.key = getSecretKeyFromString(keyString!!) 32 | this.iv = IvParameterSpec(ivString!!.toByteArray()) 33 | } 34 | 35 | fun encrypt(rawString: String): String { 36 | val cipher = Cipher.getInstance(algorithm) 37 | cipher.init(Cipher.ENCRYPT_MODE, key, iv) 38 | val cipherText = cipher.doFinal(rawString.toByteArray()) 39 | return Base64.getEncoder() 40 | .encodeToString(cipherText) 41 | } 42 | 43 | private fun getSecretKeyFromString(key: String): SecretKey { 44 | return SecretKeySpec(key.encodeToByteArray(), 0, key.length, "AES") 45 | } 46 | } 47 | 48 | val helper = AESEncryptionHelper() 49 | println() 50 | while (true) { 51 | print("Please string need to be encrypted: ") 52 | val rawString = readLine() 53 | println(helper.encrypt(rawString!!)) 54 | } 55 | -------------------------------------------------------------------------------- /backend/gradle/git-hooks/install-git-hooks.gradle: -------------------------------------------------------------------------------- 1 | task installGitHooks(type: Copy) { 2 | from 'gradle/git-hooks/pre-push' 3 | into '../.git/hooks' 4 | } 5 | 6 | build.dependsOn installGitHooks 7 | test.dependsOn installGitHooks -------------------------------------------------------------------------------- /backend/gradle/git-hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Running gradlew check before push to remote repository..." 4 | cd backend 5 | ./gradlew clean check 6 | -------------------------------------------------------------------------------- /backend/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/backend/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /backend/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /backend/mongodb-setup/config/add-user.js: -------------------------------------------------------------------------------- 1 | use 4-key-metrics; 2 | db.createUser({ user: "4km", pwd: "4000km", roles: [{ role: "readWrite", db: "4-key-metrics" } ] } ); 3 | -------------------------------------------------------------------------------- /backend/mongodb-setup/config/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sleep 15 3 | cat /app/mongo/replica-set-init.js | mongo -u admin -p root --quiet 4 | sleep 2 5 | cat /app/mongo/add-user.js | mongo -u admin -p root --quiet -------------------------------------------------------------------------------- /backend/mongodb-setup/config/keyfile.txt: -------------------------------------------------------------------------------- 1 | WI9H1+Z80akt2q8FsJ/LUVfyLK6Hg4Nh4P61/NR9dTzmOlPsrBV+GsSLEhmiG2z4 2 | t93PQ94Sn5ZsJ5R20axA07kuLguRR6q+o9rMQrO3xEKZZk4/fMorazYt7w2EWNLr 3 | YUWgf8vkp2mAkXoeNz/F15+eoQ6R9nGmpX+WjcEKMgPliZFpkMgBHnVwLVRB1YZW 4 | v2wKdPMCDOiupdqjkaG78abISgmRuu7dFZH2vKOfjEBR1DLz3qrI4KI8WXtFfdhw 5 | p6D9UbZj9jM0DqZiJaPcpYZv6+xra/rFcuz2uyW+/ohTBeOX3rPoERDvpjqSC1Zb 6 | I2NpTP0w2xZI+zzL5AkdzZTqEuLPT2obnaIbBfC5Opm7MpxmepklJ8WM59N2I7Ju 7 | 0qTOpPqXzux6jDbkkzJ9lEsTzv9An8qp/XXJtG4DWvToFFlFAR9lkCEnNidP9hEu 8 | La9iEp1YXqu+zV6GrGlYWvU1MRuxu9hNWCzxvXkZB+tuZ65U2DwgGRw35UJ9AgAC 9 | w3w0ANqwnFRhm7pN3k2fRRP+0t2M8k9BOeYfUysh4R4mUGiohvUqxqjnnj5wxEW0 10 | 0ASRT/lX7l6+4dejCmYHUwWrbQkkrsB1EJTJ4tXMcQQpMIBMwWGhF3Uip6U8xxsZ 11 | rqhYcu5NsJB0YIQqOZUWfTRVdCP44Hn/MDOg6E5x22D2zoiVTw2OgVeJdGRlTaRS 12 | JIy5h7uJ2kTotF9Hpo3sBC0XC5mycuVMtoPD2av2B34oxY1vCIkJH4mqAKuUN0kT 13 | zvhKEF3qPR7d+EBXx4a+6t9mleIlxg6Z2bKmqn3zADcwaNGRz4A/DfN0NO+o5DHn 14 | z/TtL/u4aNgLdyTgZ6WaQyXtt4RENXANc6ZqC0Ug08lyK/ihftQrWtM6M4cZWJc3 15 | gjDSD1mmjfRftyndlG///Ckt0AroQjXZ+orG+yDgwWgQmK1CTMbHXGdTAtuOUaFz 16 | vRLL0ZwBdIh9pemI21SAQZ/pfLwKp5lRB8iu/ey0KbS+H44i 17 | -------------------------------------------------------------------------------- /backend/mongodb-setup/config/replica-set-init.js: -------------------------------------------------------------------------------- 1 | rs.initiate({'_id': 'rs0', 'members': [{'_id': 0, 'host': '127.0.0.1:27017'}]}); 2 | rs.status(); 3 | -------------------------------------------------------------------------------- /backend/mongodb-setup/config/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "hellohello" 4 | chmod 400 /app/mongo/keyfile.txt 5 | chown 999 /app/mongo/keyfile.txt 6 | /usr/local/bin/docker-entrypoint.sh mongod -keyFile /app/mongo/keyfile.txt --replSet rs0 --bind_ip_all -------------------------------------------------------------------------------- /backend/mongodb-setup/mongodb-for-local/docker-compose-for-local.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | mongodb: 5 | image: mongo:4.4.4-bionic 6 | container_name: mongodb 7 | hostname: mongodb 8 | environment: 9 | MONGO_INITDB_ROOT_USERNAME: admin 10 | MONGO_INITDB_ROOT_PASSWORD: root 11 | MONGO_INITDB_DATABASE: 4-key-metrics 12 | MONGO_REPLICA_SET_NAME: rs0 13 | volumes: 14 | - ../config/:/app/mongo/ 15 | - ./mongo-db-data/:/data/db/ 16 | ports: 17 | - "27017:27017" 18 | restart: unless-stopped 19 | entrypoint: /app/mongo/startup.sh 20 | 21 | -------------------------------------------------------------------------------- /backend/mongodb-setup/mongodb-for-local/remove-mongodb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | readonly DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | 4 | export COMPOSE_PROJECT_NAME=4km-docker 5 | docker-compose -f "$DIR"/docker-compose-for-local.yml down 6 | docker stop mongodb 7 | docker rm mongodb 8 | rm -rf "$DIR"/mongo-db-data 9 | 10 | -------------------------------------------------------------------------------- /backend/mongodb-setup/mongodb-for-local/setup-mongodb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | container_name=mongodb 6 | #this command to specify the network 7 | export COMPOSE_PROJECT_NAME=4km-docker 8 | echo "checking if $container_name existance" 9 | status=$(docker inspect --format {{.State.Status}} "$container_name" | head -n 1) 10 | echo "the container $container_name status is: $status" 11 | if [[ ${status} == "running" ]]; then 12 | echo "the $container_name is already exist" 13 | exit 0 14 | fi 15 | 16 | 17 | echo "start $container_name" 18 | docker rm "${container_name}" 19 | 20 | chmod 400 "$DIR"/../config/keyfile.txt 21 | chmod +x "$DIR"/../config/*.sh 22 | 23 | docker-compose -f "$DIR"/docker-compose-for-local.yml up -d 24 | 25 | is_health_check_success=0 26 | 27 | for i in 1 2 3 4 5 28 | do 29 | echo "checking $container_name status times: $i" 30 | status=$(docker inspect --format {{.State.Status}} "$container_name" | head -n 1) 31 | echo "the container $container_name status is: $status" 32 | if [[ ${status} == "running" ]]; then 33 | is_health_check_success=1 34 | break 35 | fi 36 | sleep 1 37 | echo "continue check health for $container_name ${i} times..." 38 | done 39 | 40 | 41 | if [[ ${is_health_check_success} == 1 ]]; then 42 | echo "initializing replicaSet and add user" 43 | docker exec $container_name /app/mongo/init.sh 44 | echo "mongodb set up success ✓✓✓, databaseName=4-key-metrics, username=4km, password=4000km." 45 | else 46 | echo "$container_name set up failed XXX" 47 | exit 1 48 | fi -------------------------------------------------------------------------------- /backend/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | JAR=/app/metrik-backend.jar 6 | 7 | java ${JAVA_OPTS} -jar -Dspring.profiles.active=$APP_ENV \ 8 | -Duser.timezone=Asia/Shanghai \ 9 | -DDB_USER=$DB_USER \ 10 | -DDB_PASSWORD=$DB_PASSWORD \ 11 | -DAES_KEY=$AES_KEY \ 12 | -DAES_IV=$AES_IV \ 13 | "${JAR}" 14 | -------------------------------------------------------------------------------- /backend/scripts/backend-service-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## This script is for checking if the backend service is ready. 4 | ## arg1: Backend service host / IP 5 | ## arg2: Backend service port number 6 | ## arg3: Tolerant time (second) 7 | 8 | HOSTNAME="$1" 9 | PORT="$2" 10 | TOLERANCE="$3" 11 | 12 | HEALTH_CHECK_URL="http://$HOSTNAME:$PORT/actuator/health" 13 | 14 | COUNTER=0 15 | SERVICE_STATUS=0 16 | 17 | while [ "$SERVICE_STATUS" -eq 0 ] && [ "$COUNTER" -lt "$TOLERANCE" ]; do 18 | CURL_RESP=$(curl -s "$HEALTH_CHECK_URL" | head -n 1) 19 | 20 | if [[ "$CURL_RESP" == "{\"status\":\"UP\"}" ]]; then 21 | SERVICE_STATUS=1 22 | break 23 | fi 24 | 25 | sleep 5 26 | ((COUNTER+=5)) 27 | echo "Backend service is not ready. Waiting for retry ($COUNTER seconds so far)" 28 | done 29 | 30 | if [ "$SERVICE_STATUS" -eq 1 ]; then 31 | echo "Backend service is ready!" 32 | exit 0 33 | elif [ "$SERVICE_STATUS" -ne 1 ] || [ "$COUNTER" -ge "$TOLERANCE" ]; then 34 | echo "Backend service is down." 35 | exit 1 36 | fi 37 | -------------------------------------------------------------------------------- /backend/scripts/install-mongo-from-tarball.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | download_and_install() { 4 | echo "${PWD}/mongodb-linux-x86_64-debian10-4.4.4 directory not found. Start installation." 5 | sudo apt-get install libcurl4 openssl liblzma5 6 | wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.4.4.tgz 7 | tar -zxvf mongodb-linux-x86_64-debian10-4.4.4.tgz 8 | } 9 | 10 | [ -d "${PWD}/mongodb-linux-x86_64-debian10-4.4.4" ] \ 11 | && echo "${PWD}/mongodb-linux-x86_64-debian10-4.4.4 directory already exists. Do nothing." \ 12 | || download_and_install 13 | 14 | echo "Mongo install done!" 15 | -------------------------------------------------------------------------------- /backend/scripts/mongodb-service-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## This script is for checking if local MongoDB service is ready. 4 | ## arg1: Tolerant time (second) 5 | 6 | TOLERANCE="$1" 7 | 8 | COUNTER=0 9 | SERVICE_STATUS=0 10 | 11 | while [ "$SERVICE_STATUS" -eq 0 ] && [ "$COUNTER" -lt "$TOLERANCE" ]; do 12 | mongo --eval "db.stats()" 13 | 14 | if [ $? -eq 0 ]; then 15 | SERVICE_STATUS=1 16 | break 17 | fi 18 | 19 | sleep 2 20 | ((COUNTER+=2)) 21 | echo "MongoDB service is not available... Waiting for retry ($COUNTER seconds so far)" 22 | done 23 | 24 | if [ "$SERVICE_STATUS" -eq 1 ]; then 25 | echo "MongoDB service is ready!" 26 | exit 0 27 | elif [ "$SERVICE_STATUS" -ne 1 ] || [ "$COUNTER" -ge "$TOLERANCE" ]; then 28 | echo "MongoDB service is down." 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /backend/scripts/tcp-port-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## This script is for checking if the specific TCP port is available on HOST. 4 | ## arg1: Host / IP 5 | ## arg2: Port number you would like to check 6 | ## arg3: Tolerant time (second) 7 | 8 | HOSTNAME="$1" 9 | PORT="$2" 10 | TOLERANCE="$3" 11 | 12 | COUNTER=0 13 | PORT_STATUS=0 14 | 15 | while [ "$PORT_STATUS" -ne 0 ] && [ "$COUNTER" -lt "$TOLERANCE" ]; do 16 | nc -zv "$HOSTNAME" "$PORT" 2>&1 >/dev/null 17 | 18 | if [ $? -eq 0 ]; then 19 | PORT_STATUS=1 20 | break 21 | fi 22 | 23 | sleep 2 24 | ((COUNTER+=2)) 25 | echo "[$HOSTNAME:$PORT] is not accessible... Waiting for retry ($COUNTER seconds so far)" 26 | done 27 | 28 | if [ "$PORT_STATUS" -eq 1 ]; then 29 | echo "[$HOSTNAME:$PORT] is up!" 30 | exit 0 31 | elif [ "$PORT_STATUS" -ne 1 ] || [ "$COUNTER" -ge "$TOLERANCE" ]; then 32 | echo "[$HOSTNAME:$PORT] is down." 33 | exit 1 34 | fi -------------------------------------------------------------------------------- /backend/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "metrik-backend" 2 | -------------------------------------------------------------------------------- /backend/src/apiTest/kotlin/metrik/ProjectApiTest.kt: -------------------------------------------------------------------------------- 1 | package metrik 2 | 3 | import io.restassured.RestAssured 4 | import io.restassured.http.ContentType 5 | import metrik.base.ApiTestBase 6 | import org.hamcrest.CoreMatchers.`is` 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class ProjectApiTest : ApiTestBase() { 10 | @Test 11 | fun `should get project list returned`() { 12 | RestAssured 13 | .given() 14 | .contentType(ContentType.JSON) 15 | .get("/api/project/") 16 | .then() 17 | .statusCode(200) 18 | .body("size()", `is`(1)) 19 | .body("[0].id", `is`("601cbae825c1392117aa0429")) 20 | .body("[0].name", `is`("4-key")) 21 | .body("[0].synchronizationTimestamp", `is`(1580709600000)) 22 | } 23 | 24 | @Test 25 | fun `should get project via project ID`() { 26 | RestAssured 27 | .given() 28 | .contentType(ContentType.JSON) 29 | .get("/api/project/601cbae825c1392117aa0429") 30 | .then() 31 | .statusCode(200) 32 | .body("id", `is`("601cbae825c1392117aa0429")) 33 | .body("name", `is`("4-key")) 34 | .body("synchronizationTimestamp", `is`(1580709600000)) 35 | } 36 | 37 | @Test 38 | fun `should get 404 error returned given an invalid project ID`() { 39 | RestAssured 40 | .given() 41 | .contentType(ContentType.JSON) 42 | .get("/api/project/some-invalid-id") 43 | .then() 44 | .statusCode(404) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/apiTest/kotlin/metrik/base/ApiTestBase.kt: -------------------------------------------------------------------------------- 1 | package metrik.base 2 | 3 | import io.restassured.RestAssured 4 | import metrik.config.ApiTestConfiguration 5 | import metrik.project.domain.model.Project 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.test.autoconfigure.data.mongo.AutoConfigureDataMongo 9 | import org.springframework.boot.test.context.SpringBootTest 10 | import org.springframework.boot.web.server.LocalServerPort 11 | import org.springframework.context.annotation.Import 12 | import org.springframework.data.mongodb.core.MongoTemplate 13 | import org.springframework.test.context.ActiveProfiles 14 | 15 | @ActiveProfiles("apitest") 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 17 | @Import(ApiTestConfiguration::class) 18 | @AutoConfigureDataMongo 19 | internal class ApiTestBase { 20 | @LocalServerPort 21 | var springTestRandomServerPort = 0 22 | 23 | @Autowired 24 | lateinit var mongoTemplate: MongoTemplate 25 | 26 | @BeforeEach 27 | fun setUp() { 28 | RestAssured.port = springTestRandomServerPort 29 | 30 | mongoTemplate.save( 31 | Project( 32 | id = "601cbae825c1392117aa0429", 33 | name = "4-key", 34 | synchronizationTimestamp = 1580709600000L 35 | ) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/apiTest/kotlin/metrik/config/ApiTestConfiguration.kt: -------------------------------------------------------------------------------- 1 | package metrik.config 2 | 3 | import org.springframework.boot.test.context.TestConfiguration 4 | import org.springframework.context.annotation.Bean 5 | 6 | data class TestBean(val foo: String) 7 | 8 | @TestConfiguration 9 | class ApiTestConfiguration { 10 | @Bean 11 | fun testBean(): TestBean = TestBean("bar") 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/apiTest/resources/application-apitest.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | port: 0 5 | 6 | logging: 7 | config: classpath:log4j2-console.xml 8 | 9 | # Use length 16 for both key and IV 10 | aes: 11 | key: "aNcRfUjXn2r5u8x/" 12 | iv: "p2s5v8y/B?E(G+Kb" -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/Application.kt: -------------------------------------------------------------------------------- 1 | package metrik 2 | 3 | import org.springframework.boot.SpringApplication 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.cloud.openfeign.EnableFeignClients 6 | import org.springframework.context.annotation.EnableAspectJAutoProxy 7 | 8 | @SpringBootApplication 9 | @EnableAspectJAutoProxy 10 | @EnableFeignClients 11 | class Application 12 | 13 | fun main(args: Array) { 14 | @Suppress("SpreadOperator") 15 | SpringApplication.run(Application::class.java, *args) 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/configuration/CacheConfiguration.kt: -------------------------------------------------------------------------------- 1 | package metrik.configuration 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine 4 | import org.springframework.cache.annotation.EnableCaching 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import java.util.concurrent.TimeUnit 8 | 9 | @Configuration 10 | @EnableCaching 11 | class CacheConfiguration { 12 | @Bean 13 | fun caffeineConfiguration(): Caffeine { 14 | return Caffeine.newBuilder().expireAfterAccess(EXPIRE_TIME_MINUTES, TimeUnit.MINUTES) 15 | } 16 | 17 | companion object { 18 | private const val EXPIRE_TIME_MINUTES = 10L 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/configuration/MongoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package metrik.configuration 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.data.mongodb.MongoDatabaseFactory 6 | import org.springframework.data.mongodb.MongoTransactionManager 7 | import org.springframework.transaction.TransactionManager 8 | 9 | @Configuration 10 | class MongoConfiguration { 11 | @Bean 12 | fun transactionManager(factory: MongoDatabaseFactory?): TransactionManager? { 13 | return MongoTransactionManager(factory!!) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/configuration/RestTemplateConfiguration.kt: -------------------------------------------------------------------------------- 1 | package metrik.configuration 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.web.client.RestTemplate 6 | 7 | @Configuration 8 | class RestTemplateConfiguration { 9 | @Bean 10 | fun restTemplate(): RestTemplate { 11 | return RestTemplate() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/configuration/SwaggerUIConfiguration.kt: -------------------------------------------------------------------------------- 1 | package metrik.configuration 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import springfox.documentation.builders.PathSelectors 6 | import springfox.documentation.builders.RequestHandlerSelectors 7 | import springfox.documentation.spi.DocumentationType 8 | import springfox.documentation.spring.web.plugins.Docket 9 | import springfox.documentation.swagger2.annotations.EnableSwagger2 10 | 11 | @EnableSwagger2 12 | @Configuration 13 | class SwaggerUIConfiguration { 14 | @Bean 15 | fun api(): Docket { 16 | return Docket(DocumentationType.SWAGGER_2) 17 | .select() 18 | .apis(RequestHandlerSelectors.basePackage("metrik")) 19 | .paths(PathSelectors.any()) 20 | .build() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/exception/ApplicationException.kt: -------------------------------------------------------------------------------- 1 | package metrik.exception 2 | 3 | import org.springframework.http.HttpStatus 4 | 5 | open class ApplicationException(val httpStatus: HttpStatus, message: String) : RuntimeException(message) 6 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/exception/ErrorResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.exception 2 | 3 | class ErrorResponse(val status: Int, val message: String?) 4 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/infrastructure/serializer/NumberSerializer.kt: -------------------------------------------------------------------------------- 1 | package metrik.infrastructure.serializer 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator 4 | import com.fasterxml.jackson.databind.JsonSerializer 5 | import com.fasterxml.jackson.databind.SerializerProvider 6 | import java.math.BigDecimal 7 | import java.math.BigInteger 8 | 9 | class NumberSerializer : JsonSerializer() { 10 | override fun serialize(value: Number?, g: JsonGenerator?, serializers: SerializerProvider?) { 11 | when { 12 | value is BigDecimal -> g!!.writeNumber(value) 13 | value is BigInteger -> g!!.writeNumber(value) 14 | value is Long -> g!!.writeNumber(value) 15 | value is Double -> if (value.isNaN()) g!!.writeNumber(value) else g!!.writeNumber("%.2f".format(value)) 16 | value is Float -> if (value.isNaN()) g!!.writeNumber(value) else g!!.writeNumber("%.2f".format(value)) 17 | value !is Int && value !is Byte && value !is Short -> g!!.writeNumber(value.toString()) 18 | else -> g!!.writeNumber(value.toInt()) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/infrastructure/utlils/RequestUtil.kt: -------------------------------------------------------------------------------- 1 | package metrik.infrastructure.utlils 2 | 3 | import org.springframework.http.HttpHeaders 4 | import java.net.URL 5 | 6 | object RequestUtil { 7 | private const val HTTP_PORT = 80 8 | private const val HTTPS_PORT = 443 9 | private const val UNDEFINED_PORT = -1 10 | 11 | fun buildHeaders(headers: Map): HttpHeaders { 12 | val result = HttpHeaders() 13 | result.setAll(headers) 14 | return result 15 | } 16 | 17 | fun getDomain(pipelineURL: String): String { 18 | val url = URL(pipelineURL) 19 | 20 | var port = HTTP_PORT 21 | if (url.port != UNDEFINED_PORT) { 22 | port = url.port 23 | } else if (url.protocol.lowercase() == "https") { 24 | port = HTTPS_PORT 25 | } 26 | return "${url.protocol}://${url.host}:$port" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/infrastructure/utlils/TimeFormatExtensions.kt: -------------------------------------------------------------------------------- 1 | package metrik.infrastructure.utlils 2 | 3 | import java.time.Instant 4 | import java.time.LocalDateTime 5 | import java.time.LocalTime 6 | import java.time.OffsetDateTime 7 | import java.time.YearMonth 8 | import java.time.ZoneId 9 | import java.time.ZonedDateTime 10 | import java.util.TimeZone 11 | 12 | fun ZonedDateTime.toTimestamp(): Long = this.toInstant().toEpochMilli() 13 | 14 | fun OffsetDateTime.toTimestamp(): Long = this.toInstant().toEpochMilli() 15 | 16 | fun LocalDateTime.toDefaultZoneEpochMill(): Long = this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() 17 | 18 | fun LocalDateTime.atStartOfDay(): LocalDateTime = this.toLocalDate().atStartOfDay() 19 | 20 | fun LocalDateTime.endTimeOfSameMonth(): LocalDateTime = YearMonth.from(this).atEndOfMonth().atTime(LocalTime.MAX) 21 | 22 | fun Long.toLocalDateTime(): LocalDateTime = 23 | LocalDateTime.ofInstant(Instant.ofEpochMilli(this), TimeZone.getDefault().toZoneId()) 24 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/domain/calculator/MetricsCalculator.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.domain.calculator 2 | 3 | import metrik.metrics.domain.model.LEVEL 4 | import metrik.project.domain.model.Execution 5 | 6 | interface MetricsCalculator { 7 | fun calculateValue( 8 | allExecutions: List, 9 | startTimestamp: Long, 10 | endTimestamp: Long, 11 | pipelineStagesMap: Map 12 | ): Number 13 | 14 | fun calculateLevel(value: Number, days: Int? = 0): LEVEL 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/domain/model/CalculationPeriod.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.domain.model 2 | 3 | enum class CalculationPeriod(val timeInDays: Long) { 4 | Fortnightly(14L), Monthly(30L) 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/domain/model/Metrics.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.domain.model 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 4 | import metrik.infrastructure.serializer.NumberSerializer 5 | 6 | enum class LEVEL { 7 | ELITE, HIGH, MEDIUM, LOW, INVALID 8 | } 9 | 10 | data class Metrics( 11 | @JsonSerialize(using = NumberSerializer::class) 12 | val value: Number, 13 | val level: LEVEL?, 14 | val startTimestamp: Long, 15 | val endTimestamp: Long 16 | ) { 17 | constructor(value: Number, startTimestamp: Long, endTimestamp: Long) : this( 18 | value, 19 | null, 20 | startTimestamp, 21 | endTimestamp 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/exception/BadRequestException.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.exception 2 | 3 | import metrik.exception.ApplicationException 4 | import org.springframework.http.HttpStatus 5 | 6 | class BadRequestException(message: String) : ApplicationException(HttpStatus.BAD_REQUEST, message) 7 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/rest/MetricsController.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.rest 2 | 3 | import metrik.metrics.domain.model.CalculationPeriod 4 | import metrik.metrics.rest.vo.FourKeyMetricsResponse 5 | import metrik.metrics.rest.vo.MetricsQueryRequest 6 | import metrik.metrics.rest.vo.PipelineStageRequest 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.web.bind.annotation.GetMapping 9 | import org.springframework.web.bind.annotation.PostMapping 10 | import org.springframework.web.bind.annotation.RequestBody 11 | import org.springframework.web.bind.annotation.RequestParam 12 | import org.springframework.web.bind.annotation.RestController 13 | 14 | @RestController 15 | class MetricsController { 16 | @Autowired 17 | private lateinit var metricsApplicationService: MetricsApplicationService 18 | 19 | @PostMapping("/api/pipeline/metrics") 20 | fun getFourKeyMetrics(@RequestBody metricsQueryRequest: MetricsQueryRequest): FourKeyMetricsResponse { 21 | return metricsApplicationService.calculateFourKeyMetrics( 22 | metricsQueryRequest.pipelineStages, 23 | metricsQueryRequest.startTime, 24 | metricsQueryRequest.endTime, 25 | metricsQueryRequest.unit, 26 | ) 27 | } 28 | 29 | @GetMapping("/api/pipeline/metrics") 30 | fun getFourKeyMetrics( 31 | @RequestParam pipelineId: String, 32 | @RequestParam targetStage: String, 33 | @RequestParam startTime: Long, 34 | @RequestParam endTime: Long, 35 | @RequestParam unit: CalculationPeriod 36 | ): FourKeyMetricsResponse { 37 | return metricsApplicationService.calculateFourKeyMetrics( 38 | listOf(PipelineStageRequest(pipelineId, targetStage)), 39 | startTime, 40 | endTime, 41 | unit 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/rest/vo/FourKeyMetricsRequest.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.rest.vo 2 | 3 | import metrik.metrics.domain.model.CalculationPeriod 4 | 5 | data class MetricsQueryRequest( 6 | val startTime: Long, 7 | val endTime: Long, 8 | val unit: CalculationPeriod, 9 | val pipelineStages: List 10 | ) 11 | 12 | data class PipelineStageRequest(val pipelineId: String, val stage: String) 13 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/metrics/rest/vo/FourKeyMetricsResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.metrics.rest.vo 2 | 3 | import metrik.metrics.domain.model.Metrics 4 | 5 | data class MetricsInfo(val summary: Metrics, val details: List) 6 | 7 | data class FourKeyMetricsResponse( 8 | val deploymentFrequency: MetricsInfo, 9 | val leadTimeForChange: MetricsInfo, 10 | val meanTimeToRestore: MetricsInfo, 11 | val changeFailureRate: MetricsInfo 12 | ) 13 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/constant/GithubActionConstants.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.constant 2 | 3 | object GithubActionConstants { 4 | const val stepNumberOfFetchingNewRuns = 1 5 | const val stepNumberOfFetchingInProgressRuns = 2 6 | const val stepNumberOfFetchingCommits = 3 7 | const val totalNumberOfSteps = 3 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/model/Execution.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.model 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import org.apache.logging.log4j.util.Strings 5 | 6 | enum class Status { 7 | SUCCESS, 8 | FAILED, 9 | IN_PROGRESS, 10 | OTHER 11 | } 12 | 13 | data class Stage( 14 | val name: String = Strings.EMPTY, 15 | val status: Status = Status.FAILED, 16 | val startTimeMillis: Long = 0, 17 | val durationMillis: Long = 0, 18 | val pauseDurationMillis: Long = 0, 19 | val completedTimeMillis: Long? = null, 20 | ) { 21 | fun getStageDoneTime(): Long { 22 | return completedTimeMillis ?: (startTimeMillis + durationMillis + pauseDurationMillis) 23 | } 24 | } 25 | 26 | @JsonIgnoreProperties(ignoreUnknown = true) 27 | data class Commit( 28 | val commitId: String = Strings.EMPTY, 29 | val timestamp: Long = 0, 30 | val date: String = Strings.EMPTY, 31 | val pipelineId: String? = null 32 | ) 33 | 34 | data class Execution( 35 | val pipelineId: String = Strings.EMPTY, 36 | val number: Long = 0, 37 | val result: Status? = null, 38 | val duration: Long = 0, 39 | val timestamp: Long = 0, 40 | val url: String = Strings.EMPTY, 41 | val branch: String = Strings.EMPTY, 42 | val stages: List = emptyList(), 43 | val changeSets: List = emptyList() 44 | ) { 45 | 46 | fun containsGivenDeploymentInGivenTimeRange( 47 | deployStageName: String, 48 | status: Status, 49 | startTimestamp: Long, 50 | endTimestamp: Long 51 | ): Boolean { 52 | return stages.any { 53 | it.name == deployStageName && 54 | it.status == status && 55 | it.getStageDoneTime() in startTimestamp..endTimestamp 56 | } 57 | } 58 | 59 | fun containsGivenDeploymentBeforeGivenTimestamp( 60 | deployStageName: String, 61 | status: Status, 62 | timestamp: Long 63 | ): Boolean { 64 | return stages.any { 65 | it.name == deployStageName && 66 | it.status == status && 67 | it.getStageDoneTime() < timestamp 68 | } 69 | } 70 | 71 | fun findGivenStage(deployStageName: String, status: Status): Stage? { 72 | return stages.find { 73 | it.name == deployStageName && it.status == status 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/model/PipelineConfiguration.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.model 2 | 3 | import org.apache.logging.log4j.util.Strings 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.mongodb.core.mapping.Document 6 | 7 | @Document(collection = "pipeline") 8 | data class PipelineConfiguration( 9 | @Id 10 | val id: String = Strings.EMPTY, 11 | var projectId: String = Strings.EMPTY, 12 | val name: String = Strings.EMPTY, 13 | var username: String? = null, 14 | var credential: String = Strings.EMPTY, 15 | val url: String = Strings.EMPTY, 16 | var type: PipelineType = PipelineType.JENKINS, 17 | ) 18 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/model/PipelineType.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.model 2 | 3 | enum class PipelineType { 4 | JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, BUDDY, NOT_SUPPORTED 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/model/Project.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.model 2 | 3 | import org.apache.logging.log4j.util.Strings 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.mongodb.core.mapping.Document 6 | 7 | @Document(collection = "project") 8 | data class Project( 9 | @Id 10 | val id: String = Strings.EMPTY, 11 | var name: String = Strings.EMPTY, 12 | val synchronizationTimestamp: Long? = null, 13 | ) 14 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/repository/ProjectRepository.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.repository 2 | 3 | import metrik.project.domain.model.Project 4 | import metrik.project.exception.ProjectNotFoundException 5 | import org.slf4j.LoggerFactory 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.data.mongodb.core.MongoTemplate 8 | import org.springframework.data.mongodb.core.find 9 | import org.springframework.data.mongodb.core.query.Criteria 10 | import org.springframework.data.mongodb.core.query.Query 11 | import org.springframework.data.mongodb.core.query.Update 12 | import org.springframework.data.mongodb.core.query.isEqualTo 13 | import org.springframework.stereotype.Component 14 | 15 | @Component 16 | class ProjectRepository { 17 | @Autowired 18 | private lateinit var mongoTemplate: MongoTemplate 19 | private var logger = LoggerFactory.getLogger(this.javaClass.name) 20 | 21 | fun existWithGivenName(name: String): Boolean { 22 | val query = Query().addCriteria(Criteria.where("name").`is`(name)) 23 | return mongoTemplate.find(query).isNotEmpty() 24 | } 25 | 26 | fun findById(id: String): Project { 27 | val result = mongoTemplate.findById(id, Project::class.java) 28 | logger.info("Query result project ID [$id] is [$result]") 29 | return result ?: throw ProjectNotFoundException() 30 | } 31 | 32 | fun save(project: Project): Project { 33 | return mongoTemplate.save(project) 34 | } 35 | 36 | fun findAll(): List { 37 | val result = mongoTemplate.findAll(Project::class.java) 38 | logger.info("Query result size for all projects is [${result.size}]") 39 | return result 40 | } 41 | 42 | fun deleteById(projectId: String) { 43 | val query = Query().addCriteria(Criteria.where("id").isEqualTo(projectId)) 44 | mongoTemplate.remove(query, Project::class.java) 45 | } 46 | 47 | fun updateSynchronizationTime(projectId: String, synchronizationTimestamp: Long): Long? { 48 | val query = Query(Criteria.where("_id").`is`(projectId)) 49 | val update = Update().set("synchronizationTimestamp", synchronizationTimestamp) 50 | mongoTemplate.updateFirst(query, update, Project::class.java) 51 | return mongoTemplate.findOne(query, Project::class.java)?.synchronizationTimestamp 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/PipelineService.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service 2 | 3 | import metrik.project.domain.model.Execution 4 | import metrik.project.domain.model.PipelineConfiguration 5 | import metrik.project.rest.vo.response.SyncProgress 6 | 7 | interface PipelineService { 8 | 9 | fun syncBuildsProgressively(pipeline: PipelineConfiguration, emitCb: (SyncProgress) -> Unit): List 10 | 11 | fun verifyPipelineConfiguration(pipeline: PipelineConfiguration) 12 | 13 | fun getStagesSortedByName(pipelineId: String): List 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/buddy/BuddyDTO.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.buddy 2 | 3 | import metrik.infrastructure.utlils.toTimestamp 4 | import java.time.Duration 5 | import java.time.OffsetDateTime 6 | 7 | data class ProjectDTO( 8 | val name: String = "", 9 | val status: String = "" 10 | ) 11 | 12 | data class PipelineDTO( 13 | val id: Int = 0, 14 | val project: ProjectDTO = ProjectDTO(), 15 | ) 16 | 17 | data class ActionDTO( 18 | val name: String = "" 19 | ) 20 | 21 | data class CommitDTO( 22 | val revision: String = "", 23 | val commitDate: OffsetDateTime = OffsetDateTime.MIN 24 | ) { 25 | fun getTimestamp(): Long = commitDate.toTimestamp() 26 | fun getDateString(): String = commitDate.toString() 27 | } 28 | 29 | data class ChangeSetDTO( 30 | val commits: List = emptyList() 31 | ) 32 | 33 | data class ActionExecutionDTO( 34 | val status: String = "", 35 | val startDate: OffsetDateTime? = null, 36 | val finishDate: OffsetDateTime? = null, 37 | val action: ActionDTO = ActionDTO() 38 | ) { 39 | fun getDuration(): Long = if (startDate != null && finishDate != null) 40 | Duration.between(startDate, finishDate).toMillis() else 0 41 | 42 | fun getTimestamp(): Long = startDate?.let { it.toTimestamp() } ?: 0 43 | } 44 | 45 | data class ExecutionInfoDTO( 46 | val id: Long = 0, 47 | val url: String = "", 48 | val status: String = "", 49 | val branch: BranchDTO = BranchDTO(), 50 | val startDate: OffsetDateTime = OffsetDateTime.MIN, 51 | val finishDate: OffsetDateTime? = null, 52 | val toRevision: CommitDTO? = null, 53 | val fromRevision: CommitDTO? = null 54 | ) { 55 | fun getDuration(): Long = finishDate?.let { Duration.between(startDate, it).toMillis() } ?: 0 56 | fun getTimestamp(): Long = startDate.toTimestamp() 57 | } 58 | 59 | data class BranchDTO(val name: String = "") 60 | 61 | data class ExecutionPageDTO( 62 | val page: Int = 1, 63 | val pageSize: Int = 20, 64 | val totalPageCount: Int = 1, 65 | val elementCount: Int = 0, 66 | val totalElementCount: Int = 0, 67 | val executions: List = emptyList() 68 | ) 69 | 70 | data class ExecutionDetailsDTO( 71 | val actionExecutions: List = emptyList() 72 | ) 73 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/factory/NoopPipelineService.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.factory 2 | 3 | import metrik.project.domain.model.Execution 4 | import metrik.project.domain.model.PipelineConfiguration 5 | import metrik.project.domain.service.PipelineService 6 | import metrik.project.rest.vo.response.SyncProgress 7 | import org.slf4j.LoggerFactory 8 | import org.springframework.stereotype.Service 9 | 10 | private const val NOOP_IMPLEMENTATION = "Noop implementation" 11 | 12 | @Service("noopPipelineService") 13 | class NoopPipelineService : PipelineService { 14 | private var logger = LoggerFactory.getLogger(this.javaClass.name) 15 | 16 | override fun syncBuildsProgressively( 17 | pipeline: PipelineConfiguration, 18 | emitCb: (SyncProgress) -> Unit 19 | ): List { 20 | logger.info(NOOP_IMPLEMENTATION) 21 | return emptyList() 22 | } 23 | 24 | override fun verifyPipelineConfiguration(pipeline: PipelineConfiguration) { 25 | logger.info(NOOP_IMPLEMENTATION) 26 | } 27 | 28 | override fun getStagesSortedByName(pipelineId: String): List { 29 | logger.info(NOOP_IMPLEMENTATION) 30 | return emptyList() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/factory/PipelineServiceFactory.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.factory 2 | 3 | import metrik.project.domain.model.PipelineType 4 | import metrik.project.domain.service.PipelineService 5 | import metrik.project.domain.service.buddy.BuddyPipelineService 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.stereotype.Component 8 | 9 | @Component 10 | class PipelineServiceFactory( 11 | @Autowired private val jenkinsPipelineService: PipelineService, 12 | @Autowired private val bambooPipelineService: PipelineService, 13 | @Autowired private val githubActionsPipelineService: PipelineService, 14 | @Autowired private val bambooDeploymentPipelineService: PipelineService, 15 | @Autowired private val buddyPipelineService: BuddyPipelineService, 16 | @Autowired private val noopPipelineService: PipelineService 17 | ) { 18 | fun getService(pipelineType: PipelineType): PipelineService { 19 | return when (pipelineType) { 20 | PipelineType.JENKINS -> this.jenkinsPipelineService 21 | PipelineType.BAMBOO -> this.bambooPipelineService 22 | PipelineType.GITHUB_ACTIONS -> this.githubActionsPipelineService 23 | PipelineType.BAMBOO_DEPLOYMENT -> this.bambooDeploymentPipelineService 24 | PipelineType.BUDDY -> this.buddyPipelineService 25 | else -> this.noopPipelineService 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/githubactions/BranchService.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.githubactions 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.infrastructure.github.feign.GithubFeignClient 5 | import org.slf4j.LoggerFactory 6 | import org.springframework.stereotype.Service 7 | import java.net.URL 8 | 9 | @Service 10 | class BranchService( 11 | private val githubFeignClient: GithubFeignClient, 12 | ) { 13 | private var logger = LoggerFactory.getLogger(javaClass.name) 14 | 15 | fun retrieveBranches( 16 | pipeline: PipelineConfiguration 17 | ): List { 18 | 19 | logger.info( 20 | "Get Github Branches - " + 21 | "Sending request to Github Feign Client with owner: ${pipeline.url}, " + 22 | "branch: ${pipeline.name}" 23 | ) 24 | 25 | val branches = with(githubFeignClient) { 26 | getOwnerRepoFromUrl(pipeline.url).let { (owner, repo) -> 27 | retrieveBranches( 28 | pipeline.credential, 29 | owner, 30 | repo, 31 | ) 32 | } 33 | } 34 | return branches?.map { it.name } ?: listOf() 35 | } 36 | 37 | private fun getOwnerRepoFromUrl(url: String): Pair { 38 | val components = URL(url).path.split("/") 39 | val owner = components[components.size - ownerIndex] 40 | val repo = components.last() 41 | return Pair(owner, repo) 42 | } 43 | 44 | private companion object { 45 | const val ownerIndex = 2 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/githubactions/ExecutionConverter.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.githubactions 2 | 3 | import metrik.infrastructure.utlils.toTimestamp 4 | import metrik.project.domain.model.Commit 5 | import metrik.project.domain.model.Execution 6 | import metrik.project.domain.model.Stage 7 | import metrik.project.domain.model.Status 8 | import org.slf4j.LoggerFactory 9 | import org.springframework.stereotype.Component 10 | 11 | @Component 12 | class ExecutionConverter { 13 | private var logger = LoggerFactory.getLogger(javaClass.name) 14 | 15 | fun convertToBuild(run: GithubActionsRun, pipelineId: String, commits: List): Execution { 16 | logger.debug( 17 | "Github Actions converting: Started converting WorkflowRuns [$this] for pipeline [$pipelineId]" 18 | ) 19 | 20 | val startTimeMillis = run.createdTimestamp.toTimestamp() 21 | val completedTimeMillis = run.updatedTimestamp.toTimestamp() 22 | val durationMillis: Long = completedTimeMillis - startTimeMillis 23 | 24 | val stage: List = 25 | when (run.buildStatus) { 26 | Status.IN_PROGRESS, Status.OTHER -> emptyList() 27 | else -> listOf( 28 | Stage( 29 | run.name, 30 | run.buildStatus, 31 | startTimeMillis, 32 | durationMillis, 33 | 0, 34 | completedTimeMillis 35 | ) 36 | ) 37 | } 38 | 39 | val execution = Execution( 40 | pipelineId, 41 | run.id, 42 | run.buildStatus, 43 | durationMillis, 44 | startTimeMillis, 45 | run.url, 46 | run.branch, 47 | stage, 48 | commits 49 | ) 50 | 51 | logger.debug( 52 | "Github Actions converting: Build converted result: [$execution]" 53 | ) 54 | 55 | return execution 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/githubactions/GithubCommit.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.githubactions 2 | 3 | import java.time.ZonedDateTime 4 | 5 | data class GithubCommit( 6 | val id: String, 7 | val timestamp: ZonedDateTime 8 | ) 9 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/githubactions/Run.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.githubactions 2 | 3 | import metrik.project.domain.model.Status 4 | import java.time.ZonedDateTime 5 | 6 | enum class GithubActionsStatus(val value: String) { 7 | COMPLETED("completed"), 8 | QUEUED("queued"), 9 | IN_PROGRESS("in_progress") 10 | } 11 | 12 | enum class GithubActionsConclusion(val value: String?) { 13 | FAILURE("failure"), 14 | CANCELLED("cancelled"), 15 | SUCCESS("success"), 16 | ACTION_REQUIRED("action_required"), 17 | SKIPPED("skipped"), 18 | STALE("stale"), 19 | TIMED_OUT("timed_out"), 20 | NEUTRAL("neutral"), 21 | OTHER(null) 22 | } 23 | 24 | data class GithubActionsRun( 25 | val id: Long, 26 | val name: String, 27 | val status: String, 28 | val conclusion: String?, 29 | val url: String, 30 | val branch: String, 31 | val commitTimeStamp: ZonedDateTime, 32 | val createdTimestamp: ZonedDateTime, 33 | val updatedTimestamp: ZonedDateTime, 34 | ) { 35 | val buildStatus: Status 36 | get() = when { 37 | status == GithubActionsStatus.QUEUED.value || status == GithubActionsStatus.IN_PROGRESS.value -> 38 | Status.IN_PROGRESS 39 | conclusion == GithubActionsConclusion.SUCCESS.value -> 40 | Status.SUCCESS 41 | conclusion == GithubActionsConclusion.FAILURE.value -> 42 | Status.FAILED 43 | else -> Status.OTHER 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/domain/service/jenkins/JenkinsDTO.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.jenkins 2 | 3 | import metrik.project.domain.model.Status 4 | import org.apache.logging.log4j.util.Strings 5 | 6 | data class BuildSummaryCollectionDTO(var allBuilds: List = emptyList()) 7 | 8 | data class BuildSummaryDTO( 9 | val number: Long = 0, 10 | val result: String? = Strings.EMPTY, 11 | val duration: Long = 0, 12 | val timestamp: Long = 0, 13 | val url: String = Strings.EMPTY, 14 | val changeSets: List = emptyList() 15 | ) { 16 | fun getBuildExecutionStatus(): Status { 17 | return when (this.result) { 18 | null -> { 19 | Status.IN_PROGRESS 20 | } 21 | "SUCCESS" -> { 22 | Status.SUCCESS 23 | } 24 | "FAILURE" -> { 25 | Status.FAILED 26 | } 27 | else -> { 28 | Status.OTHER 29 | } 30 | } 31 | } 32 | } 33 | 34 | data class ChangeSetDTO(val items: List = emptyList()) 35 | 36 | data class CommitDTO(val commitId: String, val timestamp: Long, val date: String) 37 | 38 | data class BuildDetailsDTO(val stages: List = emptyList()) 39 | 40 | data class StageDTO( 41 | val name: String, 42 | val status: String?, 43 | val startTimeMillis: Long, 44 | val durationMillis: Long, 45 | val pauseDurationMillis: Long 46 | ) { 47 | fun getStageExecutionStatus(): Status { 48 | return when (this.status) { 49 | "SUCCESS" -> { 50 | Status.SUCCESS 51 | } 52 | "FAILED" -> { 53 | Status.FAILED 54 | } 55 | "IN_PROGRESS" -> { 56 | Status.IN_PROGRESS 57 | } 58 | else -> { 59 | Status.OTHER 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/exception/PipelineConfigVerifyException.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.exception 2 | 3 | import metrik.exception.ApplicationException 4 | import org.springframework.http.HttpStatus 5 | 6 | class PipelineConfigVerifyException(message: String) : ApplicationException( 7 | HttpStatus.INTERNAL_SERVER_ERROR, message 8 | ) 9 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/exception/PipelineNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.exception 2 | 3 | import metrik.exception.ApplicationException 4 | import org.springframework.http.HttpStatus 5 | 6 | class PipelineNotFoundException : ApplicationException(HttpStatus.NOT_FOUND, "Pipeline not exist") 7 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/exception/ProjectNameDuplicateException.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.exception 2 | 3 | import metrik.exception.ApplicationException 4 | import org.springframework.http.HttpStatus 5 | 6 | class ProjectNameDuplicateException : ApplicationException(HttpStatus.BAD_REQUEST, "Project name duplicate") 7 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/exception/ProjectNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.exception 2 | 3 | import metrik.exception.ApplicationException 4 | import org.springframework.http.HttpStatus 5 | 6 | class ProjectNotFoundException : ApplicationException(HttpStatus.NOT_FOUND, "Project not exist") 7 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/exception/SynchronizationException.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.exception 2 | 3 | import metrik.exception.ApplicationException 4 | import org.springframework.http.HttpStatus 5 | 6 | class SynchronizationException(message: String) : ApplicationException( 7 | HttpStatus.INTERNAL_SERVER_ERROR, message 8 | ) 9 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/infrastructure/bamboo/feign/BambooFeignClient.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.infrastructure.bamboo.feign 2 | 3 | import feign.Headers 4 | import feign.RequestInterceptor 5 | import feign.RequestTemplate 6 | import metrik.project.domain.service.bamboo.BuildDetailDTO 7 | import metrik.project.domain.service.bamboo.BuildSummaryDTO 8 | import metrik.project.domain.service.bamboo.DeployProjectDTO 9 | import metrik.project.domain.service.bamboo.DeploymentResultsDTO 10 | import metrik.project.domain.service.bamboo.DeploymentVersionBuildResultDTO 11 | import org.springframework.cloud.openfeign.FeignClient 12 | import org.springframework.web.bind.annotation.GetMapping 13 | import org.springframework.web.bind.annotation.RequestHeader 14 | import java.net.URI 15 | 16 | @FeignClient( 17 | value = "bamboo-api", 18 | url = "https://this-is-a-placeholder.com", 19 | configuration = [BambooFeignClientConfiguration::class] 20 | ) 21 | interface BambooFeignClient { 22 | @GetMapping(path = ["/rest/api/latest/project/"]) 23 | @Headers("Connection: close") 24 | fun verify( 25 | baseUrl: URI, 26 | @RequestHeader("credential") credential: String, 27 | ) 28 | 29 | @GetMapping 30 | fun getMaxBuildNumber( 31 | baseUrl: URI, 32 | @RequestHeader("credential") credential: String, 33 | ): BuildSummaryDTO? 34 | 35 | @GetMapping 36 | fun getBuildDetails( 37 | baseUrl: URI, 38 | @RequestHeader("credential") credential: String, 39 | ): BuildDetailDTO? 40 | 41 | @GetMapping 42 | fun getDeploySummary( 43 | baseUrl: URI, 44 | @RequestHeader("credential") credential: String 45 | ): DeployProjectDTO? 46 | 47 | @GetMapping 48 | fun getDeployResults( 49 | baseUrl: URI, 50 | @RequestHeader("credential") credential: String 51 | ): DeploymentResultsDTO? 52 | 53 | @GetMapping 54 | fun getDeployVersionInfo( 55 | baseUrl: URI, 56 | @RequestHeader("credential") credential: String 57 | ): DeploymentVersionBuildResultDTO? 58 | } 59 | 60 | class BambooFeignClientConfiguration : RequestInterceptor { 61 | override fun apply(template: RequestTemplate?) { 62 | val token = "Bearer " + template!!.headers()["credential"]!!.first() 63 | template.header("Authorization", token) 64 | template.removeHeader("credential") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/infrastructure/github/feign/response/BranchResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.infrastructure.github.feign.response 2 | 3 | import org.apache.logging.log4j.util.Strings 4 | 5 | data class BranchResponse( 6 | val name: String = Strings.EMPTY, 7 | ) 8 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/infrastructure/github/feign/response/CommitResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.infrastructure.github.feign.response 2 | 3 | import metrik.project.domain.service.githubactions.GithubCommit 4 | import java.time.ZonedDateTime 5 | 6 | data class CommitResponse( 7 | val sha: String, 8 | val commit: CommitInfo 9 | ) { 10 | data class CommitInfo( 11 | val committer: Committer 12 | ) { 13 | data class Committer( 14 | val date: ZonedDateTime 15 | ) 16 | } 17 | 18 | fun toGithubCommit(): GithubCommit = 19 | GithubCommit(id = sha, timestamp = commit.committer.date) 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/infrastructure/github/feign/response/MultipleRunResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.infrastructure.github.feign.response 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming 5 | 6 | @JsonNaming(SnakeCaseStrategy::class) 7 | data class MultipleRunResponse( 8 | val workflowRuns: List, 9 | val totalCount: Int 10 | ) 11 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/infrastructure/github/feign/response/SingleRunResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.infrastructure.github.feign.response 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming 5 | import metrik.project.domain.service.githubactions.GithubActionsRun 6 | import org.apache.logging.log4j.util.Strings 7 | import java.time.ZonedDateTime 8 | 9 | @JsonNaming(SnakeCaseStrategy::class) 10 | data class SingleRunResponse( 11 | val id: Long = 0, 12 | val name: String = Strings.EMPTY, 13 | val headBranch: String = Strings.EMPTY, 14 | val runNumber: Int = 0, 15 | val status: String = Strings.EMPTY, 16 | val conclusion: String? = null, 17 | val url: String = Strings.EMPTY, 18 | val headCommit: HeadCommit = HeadCommit(), 19 | val createdAt: ZonedDateTime = ZonedDateTime.now(), 20 | val updatedAt: ZonedDateTime = ZonedDateTime.now(), 21 | ) { 22 | data class HeadCommit( 23 | val id: String = Strings.EMPTY, 24 | val timestamp: ZonedDateTime = ZonedDateTime.now() 25 | ) 26 | 27 | fun toGithubActionsRun(): GithubActionsRun = 28 | GithubActionsRun( 29 | id = id, 30 | name = name, 31 | status = status, 32 | conclusion = conclusion, 33 | url = url, 34 | branch = headBranch, 35 | commitTimeStamp = headCommit.timestamp, 36 | createdTimestamp = createdAt, 37 | updatedTimestamp = updatedAt 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/infrastructure/jenkins/feign/JenkinsFeignClient.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.infrastructure.jenkins.feign 2 | 3 | import metrik.project.domain.service.jenkins.BuildDetailsDTO 4 | import metrik.project.domain.service.jenkins.BuildSummaryCollectionDTO 5 | import org.springframework.cloud.openfeign.FeignClient 6 | import org.springframework.web.bind.annotation.GetMapping 7 | import org.springframework.web.bind.annotation.PathVariable 8 | import org.springframework.web.bind.annotation.RequestHeader 9 | import org.springframework.web.bind.annotation.RequestParam 10 | import java.net.URI 11 | 12 | @FeignClient( 13 | value = "jenkins-api", 14 | url = "https://this-is-a-placeholder.com" 15 | ) 16 | interface JenkinsFeignClient { 17 | @GetMapping(path = ["/wfapi/"]) 18 | fun verifyJenkinsUrl( 19 | baseUrl: URI, 20 | @RequestHeader("Authorization") authorizationHeader: String 21 | ) 22 | 23 | @GetMapping(path = ["api/json"]) 24 | fun retrieveBuildSummariesFromJenkins( 25 | baseUrl: URI, 26 | @RequestHeader("Authorization") authorizationHeader: String, 27 | @RequestParam( 28 | "tree", 29 | required = false 30 | ) tree: String = "allBuilds[building,number,result,timestamp,duration,url," + 31 | "changeSets[items[commitId,timestamp,msg,date]]]" 32 | 33 | ): BuildSummaryCollectionDTO? 34 | 35 | @GetMapping(path = ["{buildSummaryNumber}/wfapi/describe"]) 36 | fun retrieveBuildDetailsFromJenkins( 37 | baseUrl: URI, 38 | @RequestHeader("Authorization") authorizationHeader: String, 39 | @PathVariable("buildSummaryNumber") buildSummaryNumber: Long 40 | ): BuildDetailsDTO? 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/validation/EnumConstraint.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.validation 2 | 3 | import javax.validation.Constraint 4 | import kotlin.reflect.KClass 5 | 6 | @MustBeDocumented 7 | @Constraint(validatedBy = [EnumValidator::class]) 8 | @Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | annotation class EnumConstraint( 11 | val acceptedValues: Array = [], 12 | val message: String = "must be any of value enum", 13 | val groups: Array> = [], 14 | val payload: Array> = [] 15 | ) 16 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/validation/EnumValidator.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.validation 2 | 3 | import javax.validation.ConstraintValidator 4 | import javax.validation.ConstraintValidatorContext 5 | 6 | class EnumValidator : ConstraintValidator { 7 | private lateinit var valueList: MutableList 8 | 9 | override fun initialize(constraintAnnotation: EnumConstraint) { 10 | valueList = mutableListOf() 11 | constraintAnnotation.acceptedValues.forEach { valueList.add(it.uppercase()) } 12 | } 13 | 14 | override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean { 15 | return if (value.isNullOrBlank()) { 16 | false 17 | } else { 18 | valueList.contains(value.uppercase()) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/request/BambooDeploymentRequest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.request 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.domain.model.PipelineType 5 | import javax.validation.constraints.NotBlank 6 | 7 | class BambooDeploymentPipelineRequest( 8 | @field:NotBlank(message = "Name cannot be empty") val name: String, 9 | @field:NotBlank(message = "Credential cannot be empty") val credential: String, 10 | url: String 11 | ) : PipelineRequest(url, PipelineType.BAMBOO_DEPLOYMENT.toString()) { 12 | override fun toPipeline(projectId: String, pipelineId: String): PipelineConfiguration { 13 | return with(this) { 14 | PipelineConfiguration( 15 | id = pipelineId, 16 | projectId = projectId, 17 | name = name, 18 | username = null, 19 | credential = credential, 20 | url = url, 21 | type = PipelineType.valueOf(type) 22 | ) 23 | } 24 | } 25 | } 26 | 27 | class BambooDeploymentVerificationRequest( 28 | @field:NotBlank(message = "Credential cannot be null or empty") val credential: String, 29 | url: String 30 | ) : PipelineVerificationRequest(url, PipelineType.BAMBOO_DEPLOYMENT.toString()) { 31 | override fun toPipeline() = with(this) { 32 | PipelineConfiguration( 33 | username = null, 34 | credential = credential, 35 | url = url, 36 | type = PipelineType.valueOf(type) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/request/BambooRequest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.request 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.domain.model.PipelineType 5 | import javax.validation.constraints.NotBlank 6 | 7 | class BambooPipelineRequest( 8 | @field:NotBlank(message = "Name cannot be empty") val name: String, 9 | @field:NotBlank(message = "Credential cannot be empty") val credential: String, 10 | url: String 11 | ) : PipelineRequest(url, PipelineType.BAMBOO.toString()) { 12 | override fun toPipeline(projectId: String, pipelineId: String): PipelineConfiguration { 13 | return with(this) { 14 | PipelineConfiguration( 15 | id = pipelineId, 16 | projectId = projectId, 17 | name = name, 18 | username = null, 19 | credential = credential, 20 | url = url, 21 | type = PipelineType.valueOf(type) 22 | ) 23 | } 24 | } 25 | } 26 | 27 | class BambooVerificationRequest( 28 | @field:NotBlank(message = "Credential cannot be null or empty") val credential: String, 29 | url: String 30 | ) : PipelineVerificationRequest(url, PipelineType.BAMBOO.toString()) { 31 | override fun toPipeline() = with(this) { 32 | PipelineConfiguration( 33 | username = null, 34 | credential = credential, 35 | url = url, 36 | type = PipelineType.valueOf(type) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/request/BuddyRequest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.request 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.domain.model.PipelineType 5 | import javax.validation.constraints.NotBlank 6 | 7 | class BuddyPipelineRequest( 8 | @field:NotBlank(message = "Name cannot be empty") 9 | val name: String, 10 | @field:NotBlank(message = "Credential cannot be null or empty") val credential: String, 11 | url: String 12 | ) : PipelineRequest(url, PipelineType.BUDDY.toString()) { 13 | override fun toPipeline(projectId: String, pipelineId: String) = PipelineConfiguration( 14 | id = pipelineId, 15 | projectId = projectId, 16 | name = name, 17 | credential = credential, 18 | url = url, 19 | type = PipelineType.valueOf(type) 20 | ) 21 | } 22 | 23 | class BuddyVerificationRequest( 24 | @field:NotBlank(message = "Credential cannot be null or empty") val credential: String, 25 | url: String 26 | ) : PipelineVerificationRequest(url, PipelineType.BUDDY.toString()) { 27 | override fun toPipeline() = PipelineConfiguration( 28 | credential = credential, 29 | url = url, 30 | type = PipelineType.valueOf(type) 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/request/GithubActionsRequest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.request 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.domain.model.PipelineType 5 | import java.net.URL 6 | import javax.validation.constraints.NotBlank 7 | 8 | class GithubActionsPipelineRequest( 9 | @field:NotBlank(message = "Name cannot be empty") val name: String, 10 | @field:NotBlank(message = "Credential cannot be empty") val credential: String, 11 | url: String 12 | ) : PipelineRequest(url, PipelineType.GITHUB_ACTIONS.toString()) { 13 | override fun toPipeline(projectId: String, pipelineId: String) = PipelineConfiguration( 14 | id = pipelineId, 15 | projectId = projectId, 16 | name = name, 17 | username = null, 18 | credential = credential, 19 | url = toGithubActionsUrl(url), 20 | type = PipelineType.valueOf(type) 21 | ) 22 | 23 | private fun toGithubActionsUrl(url: String) = 24 | URL(url).path.split("/").let { "$apiRepo/${it[it.size - ownerIndex]}/${it.last()}" } 25 | 26 | private companion object { 27 | const val ownerIndex = 2 28 | const val apiRepo = "https://api.github.com/repos" 29 | } 30 | } 31 | 32 | class GithubActionsVerificationRequest( 33 | @field:NotBlank(message = "Credential cannot be null or empty") val credential: String, 34 | url: String 35 | ) : PipelineVerificationRequest(url, PipelineType.GITHUB_ACTIONS.toString()) { 36 | override fun toPipeline() = PipelineConfiguration( 37 | credential = credential, 38 | url = url, 39 | type = PipelineType.valueOf(type) 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/request/JenkinsRequest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.request 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.domain.model.PipelineType 5 | import javax.validation.constraints.NotBlank 6 | 7 | class JenkinsPipelineRequest( 8 | @field:NotBlank(message = "Name cannot be empty") 9 | val name: String, 10 | @field:NotBlank(message = "Username cannot be empty") 11 | val username: String, 12 | @field:NotBlank(message = "Credential cannot be empty") 13 | val credential: String, 14 | url: String 15 | ) : PipelineRequest(url, PipelineType.JENKINS.toString()) { 16 | override fun toPipeline(projectId: String, pipelineId: String): PipelineConfiguration { 17 | return with(this) { 18 | PipelineConfiguration( 19 | id = pipelineId, 20 | projectId = projectId, 21 | name = name, 22 | username = username, 23 | credential = credential, 24 | url = url, 25 | type = PipelineType.valueOf(type) 26 | ) 27 | } 28 | } 29 | } 30 | 31 | class JenkinsVerificationRequest( 32 | @field:NotBlank(message = "Username cannot be empty") 33 | val username: String, 34 | @field:NotBlank(message = "Credential cannot be null or empty") 35 | val credential: String, 36 | url: String 37 | ) : PipelineVerificationRequest(url, PipelineType.JENKINS.toString()) { 38 | override fun toPipeline() = with(this) { 39 | PipelineConfiguration( 40 | username = username, 41 | credential = credential, 42 | url = url, 43 | type = PipelineType.valueOf(type) 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/response/PipelineResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.response 2 | 3 | import metrik.project.domain.model.PipelineConfiguration 4 | import metrik.project.domain.model.PipelineType 5 | import org.apache.logging.log4j.util.Strings 6 | 7 | data class PipelineResponse( 8 | val id: String = Strings.EMPTY, 9 | val name: String = Strings.EMPTY, 10 | val username: String? = null, 11 | val credential: String = Strings.EMPTY, 12 | val url: String = Strings.EMPTY, 13 | var type: PipelineType = PipelineType.JENKINS 14 | ) { 15 | constructor(pipeline: PipelineConfiguration) : this( 16 | pipeline.id, 17 | pipeline.name, 18 | pipeline.username, 19 | pipeline.credential, 20 | pipeline.url, 21 | pipeline.type 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/response/PipelineStagesResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.response 2 | 3 | data class PipelineStagesResponse(val pipelineId: String, val pipelineName: String, val stages: List) 4 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/response/ProjectDetailResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.response 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import metrik.project.domain.model.PipelineConfiguration 5 | import metrik.project.domain.model.Project 6 | import org.apache.logging.log4j.util.Strings 7 | 8 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 9 | data class ProjectDetailResponse( 10 | var id: String = Strings.EMPTY, 11 | var name: String = Strings.EMPTY, 12 | var synchronizationTimestamp: Long? = null, 13 | var pipelines: List = emptyList() 14 | ) { 15 | constructor(project: Project, pipelines: List) : this( 16 | id = project.id, 17 | name = project.name, 18 | synchronizationTimestamp = project.synchronizationTimestamp, 19 | pipelines = pipelines.map { PipelineResponse(it) } 20 | ) 21 | } 22 | 23 | data class ProjectSummaryResponse( 24 | var id: String = Strings.EMPTY, 25 | var name: String = Strings.EMPTY, 26 | ) { 27 | constructor(project: Project) : this(id = project.id, name = project.name) 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/response/ProjectResponse.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.response 2 | 3 | import metrik.project.domain.model.Project 4 | import org.apache.logging.log4j.util.Strings 5 | 6 | class ProjectResponse( 7 | var id: String = Strings.EMPTY, 8 | var name: String = Strings.EMPTY, 9 | var synchronizationTimestamp: Long? = null, 10 | ) { 11 | constructor(project: Project) : this() { 12 | this.id = project.id 13 | this.name = project.name 14 | this.synchronizationTimestamp = project.synchronizationTimestamp 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/metrik/project/rest/vo/response/SyncProgress.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.response 2 | 3 | data class SyncProgress( 4 | val pipelineId: String, 5 | val pipelineName: String, 6 | val progress: Int, 7 | val batchSize: Int, 8 | val step: Int? = null, 9 | val stepSize: Int? = null 10 | ) { 11 | override fun toString(): String { 12 | return "Pipeline [$pipelineId - $pipelineName]: $progress/$batchSize" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | uri: mongodb://4km:4000km@localhost:27017/4-key-metrics 5 | 6 | logging: 7 | config: classpath:log4j2-console.xml 8 | 9 | # Use length 16 for both key and IV 10 | aes: 11 | key: "&E)H@MbQeThWmZq4" 12 | iv: "D(G+KbPeShVkYp3s" 13 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-release.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | uri: mongodb://${DB_USER}:${DB_PASSWORD}@localhost:27017/4-key-metrics 5 | 6 | logging: 7 | config: classpath:log4j2-file-and-console.xml 8 | 9 | # Use length 16 for both key and IV 10 | aes: 11 | key: ${AES_KEY} 12 | iv: ${AES_IV} -------------------------------------------------------------------------------- /backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server.port: 9000 2 | spring: 3 | profiles: 4 | active: local 5 | 6 | -------------------------------------------------------------------------------- /backend/src/main/resources/log4j2-console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %p %d{yyyy-MM-dd HH:mm:ss.SSS} traceId:%X{traceId} --- [%t] %c{1.}:%m%n%ex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /backend/src/main/resources/log4j2-file-and-console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %p %d{yyyy-MM-dd HH:mm:ss.SSS} traceId:%X{traceId} --- [%t] %c{1.}:%m%n%ex 6 | 7 | /app/logs 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | ${LOG_PATTERN} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/metrik/infrastructure/encryption/AESEncryptionServiceTest.kt: -------------------------------------------------------------------------------- 1 | package metrik.infrastructure.encryption 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | internal class AESEncryptionServiceTest { 8 | private lateinit var properties: AESEncryptionProperties 9 | private lateinit var aesEncryptionService: AESEncryptionService 10 | 11 | @BeforeEach 12 | fun setUp() { 13 | properties = AESEncryptionProperties("JaNdRgUkXp2s5v8y", "KbPeShVmYq3s6v9y") 14 | aesEncryptionService = AESEncryptionService(properties) 15 | } 16 | 17 | @Test 18 | fun `should encrypt`() { 19 | assertEquals(aesEncryptionService.encrypt("test"), "wbMbbtoNKyU6tiixRfSh+Q==") 20 | } 21 | 22 | @Test 23 | fun `should not encrypt null value`() { 24 | assertEquals(aesEncryptionService.encrypt(null), null) 25 | } 26 | 27 | @Test 28 | fun `should decrypt`() { 29 | assertEquals(aesEncryptionService.decrypt("wbMbbtoNKyU6tiixRfSh+Q=="), "test") 30 | } 31 | 32 | @Test 33 | fun `should not decrypt null value`() { 34 | assertEquals(aesEncryptionService.decrypt(null), null) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/metrik/infrastructure/utlils/RequestUtilTest.kt: -------------------------------------------------------------------------------- 1 | package metrik.infrastructure.utlils 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | internal class RequestUtilTest { 7 | @Test 8 | fun `should build a bearer header`() { 9 | val header = RequestUtil.buildHeaders(mapOf(Pair("Authorization", "Bearer test"))) 10 | 11 | assertThat(header["Authorization"]!![0]).isEqualTo("Bearer test") 12 | } 13 | 14 | @Test 15 | fun `should get domain with HTTP port`() { 16 | val domain = RequestUtil.getDomain("http://www.test.com") 17 | 18 | assertThat(domain).isEqualTo("http://www.test.com:80") 19 | } 20 | 21 | @Test 22 | fun `should get domain with HTTPS port`() { 23 | val domain = RequestUtil.getDomain("https://www.test.com") 24 | 25 | assertThat(domain).isEqualTo("https://www.test.com:443") 26 | } 27 | 28 | @Test 29 | fun `should get domain with customized port`() { 30 | val domain = RequestUtil.getDomain("http://www.test.com:8090") 31 | 32 | assertThat(domain).isEqualTo("http://www.test.com:8090") 33 | } 34 | 35 | @Test 36 | fun `should get domain with HTTPS protocol`() { 37 | val domain = RequestUtil.getDomain("https://www.test.com") 38 | 39 | assertThat(domain).isEqualTo("https://www.test.com:443") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/metrik/infrastructure/utlils/TimeFormatUtilTest.kt: -------------------------------------------------------------------------------- 1 | package metrik.infrastructure.utlils 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | import java.time.ZoneId 6 | import java.time.ZonedDateTime 7 | 8 | internal class TimeFormatUtilTest { 9 | @Test 10 | fun `should convert zonedDateTime to timestamp`() { 11 | val date: ZonedDateTime = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")) 12 | assertEquals(1514764800000, date.toTimestamp()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/metrik/project/domain/model/StageTest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.model 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | 6 | internal class StageTest { 7 | @Test 8 | fun `should return completedTime when stage contains completedTime`() { 9 | assertEquals( 10 | 1610700490630, 11 | Stage( 12 | startTimeMillis = 1610700490500, 13 | durationMillis = 129, 14 | pauseDurationMillis = 1, 15 | completedTimeMillis = 1610700490630 16 | ).getStageDoneTime() 17 | ) 18 | } 19 | 20 | @Test 21 | fun `should return sum of startTime and durationTime and pauseTime given stage does not contain completedTime`() { 22 | assertEquals( 23 | 1610700490630, 24 | Stage( 25 | startTimeMillis = 1610700490500, 26 | durationMillis = 129, 27 | pauseDurationMillis = 1, 28 | completedTimeMillis = null 29 | ).getStageDoneTime() 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/metrik/project/domain/service/githubactions/BranchServiceTest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.domain.service.githubactions 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.InjectMockKs 5 | import io.mockk.impl.annotations.MockK 6 | import io.mockk.junit5.MockKExtension 7 | import metrik.project.TestFixture.branch1 8 | import metrik.project.TestFixture.branch2 9 | import metrik.project.domain.model.PipelineConfiguration 10 | import metrik.project.infrastructure.github.feign.GithubFeignClient 11 | import org.assertj.core.api.Assertions 12 | import org.junit.jupiter.api.Test 13 | import org.junit.jupiter.api.extension.ExtendWith 14 | 15 | @ExtendWith(MockKExtension::class) 16 | internal class BranchServiceTest { 17 | @InjectMockKs 18 | private lateinit var branchService: BranchService 19 | 20 | @MockK 21 | private lateinit var githubFeignClient: GithubFeignClient 22 | 23 | private val testPipeline = 24 | PipelineConfiguration(id = "test pipeline", credential = "fake token", url = "https://test.com/test/test") 25 | 26 | @Test 27 | fun `should get all branches`() { 28 | every { 29 | githubFeignClient.retrieveBranches( 30 | credential = any(), owner = any(), repo = any() 31 | ) 32 | } returns listOf( 33 | branch1, 34 | branch2 35 | ) 36 | 37 | val branches = branchService.retrieveBranches(testPipeline) 38 | 39 | Assertions.assertThat(branches.size).isEqualTo(2) 40 | Assertions.assertThat(branches[0]).isEqualTo(branch1.name) 41 | Assertions.assertThat(branches[1]).isEqualTo(branch2.name) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/metrik/project/rest/vo/request/GithubActionsPipelineRequestTest.kt: -------------------------------------------------------------------------------- 1 | package metrik.project.rest.vo.request 2 | 3 | import io.mockk.junit5.MockKExtension 4 | import metrik.project.TestFixture.buildGithubActionsPipelineRequest 5 | import metrik.project.TestFixture.githubActionsPipeline 6 | import metrik.project.TestFixture.pipelineId 7 | import metrik.project.TestFixture.projectId 8 | import org.junit.jupiter.api.Assertions.assertEquals 9 | import org.junit.jupiter.api.Assertions.assertThrows 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | 13 | @ExtendWith(MockKExtension::class) 14 | internal class GithubActionsPipelineRequestTest { 15 | 16 | @Test 17 | fun `should convert githubActions request to pipeline`() { 18 | val githubActionsPipelineRequest = buildGithubActionsPipelineRequest() 19 | val pipeline = githubActionsPipelineRequest.toPipeline(projectId, pipelineId) 20 | assertEquals(pipeline, githubActionsPipeline) 21 | } 22 | 23 | @Test 24 | fun `should throw exception when convert wrong githubActions url`() { 25 | val githubActionsPipelineRequest = GithubActionsPipelineRequest( 26 | name = "pipeline", 27 | credential = "credential", 28 | url = "https://github.com" 29 | ) 30 | assertThrows( 31 | IndexOutOfBoundsException::class.java 32 | ) { 33 | githubActionsPipelineRequest.toPipeline(projectId, pipelineId) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | aes: 2 | key: "JaNdRgUkXp2s5v8y" 3 | iv: "KbPeShVmYq3s6v9y" 4 | spring: 5 | data: 6 | mongodb: 7 | uri: mongodb://4km:fake@localhost:27017/fake-db -------------------------------------------------------------------------------- /backend/src/test/resources/calculator/builds-for-MLT-case-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "1", 4 | "number": 1, 5 | "duration": 60420, 6 | "result": "SUCCESS", 7 | "timestamp": 3, 8 | "url": "http://localhost:8001/job/4km-backend/1/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "build", 13 | "startTimeMillis": 4, 14 | "durationMillis": 1, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "SUCCESS", 19 | "name": "deploy to prod", 20 | "startTimeMillis": 6, 21 | "durationMillis": 1, 22 | "pauseDurationMillis": 1 23 | } 24 | ], 25 | "changeSets": [ 26 | { 27 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 28 | "msg": "this is a message", 29 | "timestamp": 1, 30 | "date": "2021-01-01 00:00:00 +0800" 31 | }, 32 | { 33 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 34 | "msg": "this is a message", 35 | "timestamp": 2, 36 | "date": "2021-01-01 00:00:00 +0800" 37 | } 38 | ] 39 | }, 40 | { 41 | "pipelineId": "2", 42 | "number": 1, 43 | "duration": 60420, 44 | "result": "SUCCESS", 45 | "timestamp": 3, 46 | "url": "http://localhost:8001/job/4km-backend/1/", 47 | "stages": [ 48 | { 49 | "status": "SUCCESS", 50 | "name": "build", 51 | "startTimeMillis": 4, 52 | "durationMillis": 1, 53 | "pauseDurationMillis": 0 54 | }, 55 | { 56 | "status": "SUCCESS", 57 | "name": "deploy to prod", 58 | "startTimeMillis": 6, 59 | "durationMillis": 1, 60 | "pauseDurationMillis": 1 61 | } 62 | ], 63 | "changeSets": [ 64 | { 65 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 66 | "msg": "this is a message", 67 | "timestamp": 1, 68 | "date": "2021-01-01 00:00:00 +0800" 69 | }, 70 | { 71 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 72 | "msg": "this is a message", 73 | "timestamp": 2, 74 | "date": "2021-01-01 00:00:00 +0800" 75 | } 76 | ] 77 | } 78 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/calculator/builds-for-MLT-case-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "1", 4 | "number": 1, 5 | "duration": 60420, 6 | "result": "SUCCESS", 7 | "timestamp": 3, 8 | "url": "http://localhost:8001/job/4km-backend/1/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "build", 13 | "startTimeMillis": 4, 14 | "durationMillis": 1, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "SUCCESS", 19 | "name": "deploy to prod", 20 | "startTimeMillis": 6, 21 | "durationMillis": 1, 22 | "pauseDurationMillis": 1 23 | } 24 | ], 25 | "changeSets": [ 26 | { 27 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 28 | "msg": "this is a message", 29 | "timestamp": 1, 30 | "date": "2021-01-01 00:00:00 +0800" 31 | }, 32 | { 33 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 34 | "msg": "this is a message", 35 | "timestamp": 2, 36 | "date": "2021-01-01 00:00:00 +0800" 37 | } 38 | ] 39 | }, 40 | { 41 | "pipelineId": "2", 42 | "number": 1, 43 | "duration": 60420, 44 | "result": "SUCCESS", 45 | "timestamp": 3, 46 | "url": "http://localhost:8001/job/4km-backend/1/", 47 | "stages": [ 48 | { 49 | "status": "SUCCESS", 50 | "name": "build", 51 | "startTimeMillis": 4, 52 | "durationMillis": 1, 53 | "pauseDurationMillis": 0 54 | }, 55 | { 56 | "status": "SUCCESS", 57 | "name": "deploy to prod", 58 | "startTimeMillis": 6, 59 | "durationMillis": 1, 60 | "pauseDurationMillis": 1 61 | } 62 | ], 63 | "changeSets": [ 64 | { 65 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 66 | "msg": "this is a message", 67 | "timestamp": 6, 68 | "date": "2021-01-01 00:00:00 +0800" 69 | } 70 | ] 71 | } 72 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/calculator/builds-for-MLT-case-3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "1", 4 | "number": 1, 5 | "duration": 60420, 6 | "result": "SUCCESS", 7 | "timestamp": 3, 8 | "url": "http://localhost:8001/job/4km-backend/1/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "build", 13 | "startTimeMillis": 4, 14 | "durationMillis": 1, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "SUCCESS", 19 | "name": "deploy to prod", 20 | "startTimeMillis": 6, 21 | "durationMillis": 1, 22 | "pauseDurationMillis": 1 23 | } 24 | ], 25 | "changeSets": [ 26 | { 27 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 28 | "msg": "this is a message", 29 | "timestamp": 1, 30 | "date": "2021-01-01 00:00:00 +0800" 31 | }, 32 | { 33 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 34 | "msg": "this is a message", 35 | "timestamp": 2, 36 | "date": "2021-01-01 00:00:00 +0800" 37 | } 38 | ] 39 | }, 40 | { 41 | "pipelineId": "2", 42 | "number": 1, 43 | "duration": 60420, 44 | "result": "SUCCESS", 45 | "timestamp": 3, 46 | "url": "http://localhost:8001/job/4km-backend/1/", 47 | "stages": [ 48 | { 49 | "status": "SUCCESS", 50 | "name": "build", 51 | "startTimeMillis": 4, 52 | "durationMillis": 1, 53 | "pauseDurationMillis": 0 54 | }, 55 | { 56 | "status": "SUCCESS", 57 | "name": "deploy to prod", 58 | "startTimeMillis": 6, 59 | "durationMillis": 1, 60 | "pauseDurationMillis": 1 61 | } 62 | ], 63 | "changeSets": [ 64 | { 65 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 66 | "msg": "this is a message", 67 | "timestamp": 6, 68 | "date": "2021-01-01 00:00:00 +0800" 69 | } 70 | ] 71 | } 72 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/calculator/builds-for-MLT-case-4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "1", 4 | "number": 1, 5 | "duration": 60420, 6 | "result": "SUCCESS", 7 | "timestamp": 3, 8 | "url": "http://localhost:8001/job/4km-backend/1/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "build", 13 | "startTimeMillis": 4, 14 | "durationMillis": 1, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "SUCCESS", 19 | "name": "deploy to prod", 20 | "startTimeMillis": 6, 21 | "durationMillis": 1, 22 | "pauseDurationMillis": 1 23 | } 24 | ], 25 | "changeSets": [ 26 | { 27 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 28 | "msg": "this is a message", 29 | "timestamp": 1, 30 | "date": "2021-01-01 00:00:00 +0800" 31 | }, 32 | { 33 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 34 | "msg": "this is a message", 35 | "timestamp": 2, 36 | "date": "2021-01-01 00:00:00 +0800" 37 | } 38 | ] 39 | }, 40 | { 41 | "pipelineId": "2", 42 | "number": 1, 43 | "duration": 60420, 44 | "result": "SUCCESS", 45 | "timestamp": 3, 46 | "url": "http://localhost:8001/job/4km-backend/1/", 47 | "stages": [ 48 | { 49 | "status": "SUCCESS", 50 | "name": "build", 51 | "startTimeMillis": 4, 52 | "durationMillis": 1, 53 | "pauseDurationMillis": 0 54 | }, 55 | { 56 | "status": "SUCCESS", 57 | "name": "deploy to prod", 58 | "startTimeMillis": 6, 59 | "durationMillis": 1, 60 | "pauseDurationMillis": 1 61 | } 62 | ], 63 | "changeSets": [ 64 | { 65 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 66 | "msg": "this is a message", 67 | "timestamp": 6, 68 | "date": "2021-01-01 00:00:00 +0800" 69 | } 70 | ] 71 | } 72 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/calculator/builds-for-MLT-case-5-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "1", 4 | "number": 1, 5 | "duration": 60420, 6 | "result": "SUCCESS", 7 | "timestamp": 8, 8 | "url": "http://localhost:8001/job/4km-backend/1/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "build", 13 | "startTimeMillis": 4, 14 | "durationMillis": 1, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "SUCCESS", 19 | "name": "deploy to prod", 20 | "startTimeMillis": 6, 21 | "durationMillis": 1, 22 | "pauseDurationMillis": 1 23 | } 24 | ], 25 | "changeSets": [ 26 | { 27 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 28 | "msg": "this is a message", 29 | "timestamp": 8, 30 | "date": "2021-01-01 00:00:00 +0800" 31 | }, 32 | { 33 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 34 | "msg": "this is a message", 35 | "timestamp": 8, 36 | "date": "2021-01-01 00:00:00 +0800" 37 | } 38 | ] 39 | }, 40 | { 41 | "pipelineId": "2", 42 | "number": 1, 43 | "duration": 60420, 44 | "result": "SUCCESS", 45 | "timestamp": 8, 46 | "url": "http://localhost:8001/job/4km-backend/1/", 47 | "stages": [ 48 | { 49 | "status": "SUCCESS", 50 | "name": "build", 51 | "startTimeMillis": 4, 52 | "durationMillis": 1, 53 | "pauseDurationMillis": 0 54 | }, 55 | { 56 | "status": "SUCCESS", 57 | "name": "deploy to prod", 58 | "startTimeMillis": 6, 59 | "durationMillis": 1, 60 | "pauseDurationMillis": 1 61 | } 62 | ], 63 | "changeSets": [ 64 | { 65 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 66 | "msg": "this is a message", 67 | "timestamp": 8, 68 | "date": "2021-01-01 00:00:00 +0800" 69 | }, 70 | { 71 | "commitId": "b9b775059d120a0dbf09fd40d2becea69a112345", 72 | "msg": "this is a message", 73 | "timestamp": 8, 74 | "date": "2021-01-01 00:00:00 +0800" 75 | } 76 | ] 77 | } 78 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 1, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "plan1", 15 | "shortKey": "PLAN1", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:8002/rest/api/latest/plan/fake-plan-key1", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key1", 23 | "name": "MULTI_REPO - plan1", 24 | "planKey": { 25 | "key": "fake-plan-key1" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key1-1", 29 | "lifeCycleState": "Finished", 30 | "id": 3473612, 31 | "specsResult": false, 32 | "key": "fake-plan-key1-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key1-1", 35 | "entityKey": { 36 | "key": "fake-plan-key1" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Successful", 41 | "buildState": "Successful", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:8002/rest/api/latest/result/fake-plan-key1", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 1, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "plan1", 15 | "shortKey": "PLAN1", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:8002/rest/api/latest/plan/fake-plan-key1", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key1", 23 | "name": "MULTI_REPO - plan1", 24 | "planKey": { 25 | "key": "fake-plan-key1" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key1-1", 29 | "lifeCycleState": "Finished", 30 | "id": 3473612, 31 | "specsResult": false, 32 | "key": "fake-plan-key1-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key1-1", 35 | "entityKey": { 36 | "key": "fake-plan-key1" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Successful", 41 | "buildState": "Successful", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:8002/rest/api/latest/result/fake-plan-key1", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 1, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "plan1", 15 | "shortKey": "PLAN1", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:80/rest/api/latest/plan/fake-plan-key", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key", 23 | "name": "MULTI_REPO - plan1", 24 | "planKey": { 25 | "key": "fake-plan-key" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key-1", 29 | "lifeCycleState": "Finished", 30 | "id": 5570566, 31 | "specsResult": false, 32 | "key": "fake-plan-key-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key-1", 35 | "entityKey": { 36 | "key": "fake-plan-key" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Failed", 41 | "buildState": "Failed", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:8002/rest/api/latest/result/fake-plan-key1", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 1, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "plan1", 15 | "shortKey": "PLAN1", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:80/rest/api/latest/plan/fake-plan-key", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key", 23 | "name": "MULTI_REPO - plan1", 24 | "planKey": { 25 | "key": "fake-plan-key" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key-1", 29 | "lifeCycleState": "Finished", 30 | "id": 5570566, 31 | "specsResult": false, 32 | "key": "fake-plan-key-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key-1", 35 | "entityKey": { 36 | "key": "fake-plan-key" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Successful", 41 | "buildState": "Successful", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:8002/rest/api/latest/result/fake-plan-key1", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-5.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 1, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "plan1", 15 | "shortKey": "PLAN1", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:80/rest/api/latest/plan/fake-plan-key", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key", 23 | "name": "MULTI_REPO - plan1", 24 | "planKey": { 25 | "key": "fake-plan-key" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key-1", 29 | "lifeCycleState": "Finished", 30 | "id": 5570566, 31 | "specsResult": false, 32 | "key": "fake-plan-key-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key-1", 35 | "entityKey": { 36 | "key": "fake-plan-key" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Failed", 41 | "buildState": "Failed", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:8002/rest/api/latest/result/fake-plan-key1", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-6.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 25, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "test", 15 | "shortKey": "TEST", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:80/rest/api/latest/plan/fake-plan-key", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key", 23 | "name": "Four-Key-Metrics - test", 24 | "planKey": { 25 | "key": "fake-plan-key" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key-1", 29 | "lifeCycleState": "Finished", 30 | "id": 6619139, 31 | "specsResult": false, 32 | "key": "fake-plan-key-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key-1", 35 | "entityKey": { 36 | "key": "fake-plan-key" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Successful", 41 | "buildState": "Successful", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-7.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 25, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "test", 15 | "shortKey": "TEST", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:80/rest/api/latest/plan/fake-plan-key", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key", 23 | "name": "Four-Key-Metrics - test", 24 | "planKey": { 25 | "key": "fake-plan-key" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key-1", 29 | "lifeCycleState": "Finished", 30 | "id": 6619139, 31 | "specsResult": false, 32 | "key": "fake-plan-key-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key-1", 35 | "entityKey": { 36 | "key": "fake-plan-key" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Successful", 41 | "buildState": "Successful", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-build-summary-8.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "size": 1, 4 | "expand": "result", 5 | "start-index": 0, 6 | "max-result": 1, 7 | "result": [ 8 | { 9 | "link": { 10 | "href": "http://localhost:80/rest/api/latest/result/fake-plan-key-1", 11 | "rel": "self" 12 | }, 13 | "plan": { 14 | "shortName": "plan1", 15 | "shortKey": "PLAN1", 16 | "type": "chain", 17 | "enabled": true, 18 | "link": { 19 | "href": "http://localhost:80/rest/api/latest/plan/fake-plan-key", 20 | "rel": "self" 21 | }, 22 | "key": "fake-plan-key", 23 | "name": "MULTI_REPO - plan1", 24 | "planKey": { 25 | "key": "fake-plan-key" 26 | } 27 | }, 28 | "buildResultKey": "fake-plan-key-1", 29 | "lifeCycleState": "Finished", 30 | "id": 5570566, 31 | "specsResult": false, 32 | "key": "fake-plan-key-1", 33 | "planResultKey": { 34 | "key": "fake-plan-key-1", 35 | "entityKey": { 36 | "key": "fake-plan-key" 37 | }, 38 | "resultNumber": 1 39 | }, 40 | "state": "Failed", 41 | "buildState": "Failed", 42 | "number": 1, 43 | "buildNumber": 1 44 | } 45 | ] 46 | }, 47 | "expand": "results", 48 | "link": { 49 | "href": "http://localhost:8002/rest/api/latest/result/fake-plan-key1", 50 | "rel": "self" 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-deploy-project-summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1736705, 3 | "oid": "1atgs9hfrmm0x", 4 | "key": { 5 | "key": "1736705" 6 | }, 7 | "name": "deploy-this", 8 | "planKey": { 9 | "key": "SEC-DEP" 10 | }, 11 | "description": "", 12 | "environments": [ 13 | { 14 | "id": 1802241, 15 | "key": { 16 | "key": "1736705-1802241" 17 | }, 18 | "name": "dev", 19 | "description": "", 20 | "deploymentProjectId": 1736705, 21 | "operations": { 22 | "canView": true, 23 | "canViewConfiguration": true, 24 | "canEdit": true, 25 | "canDelete": true, 26 | "allowedToExecute": true, 27 | "canExecute": true, 28 | "allowedToCreateVersion": false, 29 | "allowedToSetVersionStatus": false 30 | }, 31 | "position": 0, 32 | "configurationState": "TASKED" 33 | }, 34 | { 35 | "id": 1802242, 36 | "key": { 37 | "key": "1736705-1802242" 38 | }, 39 | "name": "staging", 40 | "description": "", 41 | "deploymentProjectId": 1736705, 42 | "operations": { 43 | "canView": true, 44 | "canViewConfiguration": true, 45 | "canEdit": true, 46 | "canDelete": true, 47 | "allowedToExecute": true, 48 | "canExecute": true, 49 | "allowedToCreateVersion": false, 50 | "allowedToSetVersionStatus": false 51 | }, 52 | "position": 1, 53 | "configurationState": "TASKED" 54 | } 55 | ], 56 | "operations": { 57 | "canView": true, 58 | "canViewConfiguration": true, 59 | "canEdit": true, 60 | "canDelete": true, 61 | "allowedToExecute": false, 62 | "canExecute": false, 63 | "allowedToCreateVersion": true, 64 | "allowedToSetVersionStatus": false 65 | }, 66 | "repositorySpecsManaged": false 67 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-deploy-version-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploymentVersion": { 3 | "id": 1835009, 4 | "name": "release-1", 5 | "creationDate": 1631501012686, 6 | "creatorUserName": "devon", 7 | "items": [], 8 | "operations": { 9 | "canView": true, 10 | "canViewConfiguration": true, 11 | "canEdit": true, 12 | "canDelete": true, 13 | "allowedToExecute": false, 14 | "canExecute": false, 15 | "allowedToCreateVersion": true, 16 | "allowedToSetVersionStatus": true 17 | }, 18 | "creatorDisplayName": "devonzhang", 19 | "creatorGravatarUrl": "https://secure.gravatar.com/avatar/4459464f71cd17829d23981b93f6e155?r=g&s=24&d=mm", 20 | "planBranchName": "master", 21 | "ageZeroPoint": 1631501066173 22 | }, 23 | "planResultKey": { 24 | "key": "SEC-DEP-3", 25 | "entityKey": { 26 | "key": "SEC-DEP" 27 | }, 28 | "resultNumber": 3 29 | } 30 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/bamboo/raw-deploy-version-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploymentVersion": { 3 | "id": 2064385, 4 | "name": "release-2", 5 | "creationDate": 1631513979295, 6 | "creatorUserName": "devon", 7 | "items": [], 8 | "operations": { 9 | "canView": true, 10 | "canViewConfiguration": true, 11 | "canEdit": true, 12 | "canDelete": true, 13 | "allowedToExecute": false, 14 | "canExecute": false, 15 | "allowedToCreateVersion": true, 16 | "allowedToSetVersionStatus": true 17 | }, 18 | "creatorDisplayName": "devonzhang", 19 | "creatorGravatarUrl": "https://secure.gravatar.com/avatar/4459464f71cd17829d23981b93f6e155?r=g&s=24&d=mm", 20 | "planBranchName": "master", 21 | "ageZeroPoint": 1631513979530 22 | }, 23 | "planResultKey": { 24 | "key": "SEC-DEP-3", 25 | "entityKey": { 26 | "key": "SEC-DEP" 27 | }, 28 | "resultNumber": 3 29 | } 30 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/buddy/executions-empty-unpaged.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.sls.io/workspaces/beta/projects/metrik/pipelines/124763/executions", 3 | "executions": [] 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/buddy/executions-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.sls.io/workspaces/beta/projects/metrik/pipelines/124763/executions", 3 | "page": 1, 4 | "page_size": 20, 5 | "total_page_count": 0, 6 | "element_count": 0, 7 | "total_element_count": 0, 8 | "executions": [] 9 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/buddy/verify-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.sls.io/workspaces/beta/projects/metrik", 3 | "html_url": "https://sls.io/beta/metrik", 4 | "name": "metrik", 5 | "display_name": "metrik", 6 | "status": "ACTIVE", 7 | "create_date": "2022-09-15T06:14:12Z", 8 | "created_by": { 9 | "url": "https://api.sls.io/workspaces/beta/members/123664", 10 | "html_url": "https://sls.io/beta/profile/123664", 11 | "id": 123664, 12 | "name": "razu", 13 | "avatar_url": "https://sls.io/image-server/user/0/1/2/3/6/6/4/06dd8013152fe6af3fe59e4f89984ff0/w/32/32/AVATAR.png?ts=1597056370750", 14 | "admin": false, 15 | "workspace_owner": false 16 | }, 17 | "http_repository": "https://sls.io/beta/metrik", 18 | "ssh_repository": "buddy@sls.io:beta/metrik", 19 | "default_branch": "main", 20 | "update_default_branch_from_external": true 21 | } -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/githubactions/commits/empty-commit.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/githubactions/runs/empty-run.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 2, 3 | "workflow_runs": [ 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/githubactions/verify-pipeline/runs-verify1.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 2 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/githubactions/verify-pipeline/runs-verify2.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 4 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/expected/builds-for-jenkins-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "fake pipeline", 4 | "number": 82, 5 | "duration": 60420, 6 | "result": "OTHER", 7 | "timestamp": 1610949317906, 8 | "url": "http://localhost:8001/job/4km-backend/82/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "Declarative: Checkout SCM", 13 | "startTimeMillis": 1610949320066, 14 | "durationMillis": 3720, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "FAILED", 19 | "name": "Build Docker image", 20 | "startTimeMillis": 1610949324441, 21 | "durationMillis": 53584, 22 | "pauseDurationMillis": 0 23 | }, 24 | { 25 | "status": "OTHER", 26 | "name": "Push Docker image", 27 | "startTimeMillis": 1610949378045, 28 | "durationMillis": 36, 29 | "pauseDurationMillis": 0 30 | }, 31 | { 32 | "status": "OTHER", 33 | "name": "Deploy to DEV", 34 | "startTimeMillis": 1610949378101, 35 | "durationMillis": 30, 36 | "pauseDurationMillis": 0 37 | }, 38 | { 39 | "status": "IN_PROGRESS", 40 | "name": "Deploy to UAT", 41 | "startTimeMillis": 1610949378152, 42 | "durationMillis": 29, 43 | "pauseDurationMillis": 0 44 | } 45 | ], 46 | "changeSets": [ 47 | { 48 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 49 | "timestamp": 1610944812000, 50 | "date": "2021-01-18 12:40:12 +0800", 51 | "msg": "[Fireman] add check status when deploy to dev" 52 | } 53 | ] 54 | } 55 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/expected/builds-for-jenkins-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "fake pipeline", 4 | "number": 1, 5 | "duration": 60420, 6 | "result": "ABORTED", 7 | "timestamp": 1610949317906, 8 | "url": "http://localhost:8001/job/4km-backend/1/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "Declarative: Checkout SCM", 13 | "startTimeMillis": 1610949320066, 14 | "durationMillis": 3720, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "ABORTED", 19 | "name": "Build Docker image", 20 | "startTimeMillis": 1610949324441, 21 | "durationMillis": 53584, 22 | "pauseDurationMillis": 0 23 | }, 24 | { 25 | "status": "ABORTED", 26 | "name": "Push Docker image", 27 | "startTimeMillis": 1610949378045, 28 | "durationMillis": 36, 29 | "pauseDurationMillis": 0 30 | }, 31 | { 32 | "status": "ABORTED", 33 | "name": "Deploy to DEV", 34 | "startTimeMillis": 1610949378101, 35 | "durationMillis": 30, 36 | "pauseDurationMillis": 0 37 | }, 38 | { 39 | "status": "ABORTED", 40 | "name": "Deploy to UAT", 41 | "startTimeMillis": 1610949378152, 42 | "durationMillis": 29, 43 | "pauseDurationMillis": 0 44 | } 45 | ], 46 | "changeSets": [ 47 | { 48 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 49 | "timestamp": 1610944812000, 50 | "date": "2021-01-18 12:40:12 +0800", 51 | "msg": "[Fireman] add check status when deploy to dev" 52 | } 53 | ] 54 | } 55 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/expected/builds-for-jenkins-3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "fake pipeline", 4 | "number": 82, 5 | "duration": 60420, 6 | "result": "FAILED", 7 | "timestamp": 1610949317906, 8 | "url": "http://localhost:8001/job/4km-backend/82/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "Declarative: Checkout SCM", 13 | "startTimeMillis": 1610949320066, 14 | "durationMillis": 3720, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "OTHER", 19 | "name": "Build Docker image", 20 | "startTimeMillis": 1610949324441, 21 | "durationMillis": 53584, 22 | "pauseDurationMillis": 0 23 | }, 24 | { 25 | "status": "OTHER", 26 | "name": "Push Docker image", 27 | "startTimeMillis": 1610949378045, 28 | "durationMillis": 36, 29 | "pauseDurationMillis": 0 30 | }, 31 | { 32 | "status": "OTHER", 33 | "name": "Deploy to DEV", 34 | "startTimeMillis": 1610949378101, 35 | "durationMillis": 30, 36 | "pauseDurationMillis": 0 37 | }, 38 | { 39 | "status": "OTHER", 40 | "name": "Deploy to UAT", 41 | "startTimeMillis": 1610949378152, 42 | "durationMillis": 29, 43 | "pauseDurationMillis": 0 44 | } 45 | ], 46 | "changeSets": [ 47 | { 48 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 49 | "timestamp": 1610944812000, 50 | "date": "2021-01-18 12:40:12 +0800", 51 | "msg": "[Fireman] add check status when deploy to dev" 52 | } 53 | ] 54 | } 55 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/expected/builds-for-jenkins-4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "fake pipeline", 4 | "number": 82, 5 | "duration": 60420, 6 | "timestamp": 1610949317906, 7 | "url": "http://localhost:8001/job/4km-backend/82/", 8 | "stages": [ 9 | { 10 | "status": "SUCCESS", 11 | "name": "Declarative: Checkout SCM", 12 | "startTimeMillis": 1610949320066, 13 | "durationMillis": 3720, 14 | "pauseDurationMillis": 0 15 | }, 16 | { 17 | "status": "OTHER", 18 | "name": "Build Docker image", 19 | "startTimeMillis": 1610949324441, 20 | "durationMillis": 53584, 21 | "pauseDurationMillis": 0 22 | }, 23 | { 24 | "status": "OTHER", 25 | "name": "Push Docker image", 26 | "startTimeMillis": 1610949378045, 27 | "durationMillis": 36, 28 | "pauseDurationMillis": 0 29 | }, 30 | { 31 | "status": "OTHER", 32 | "name": "Deploy to DEV", 33 | "startTimeMillis": 1610949378101, 34 | "durationMillis": 30, 35 | "pauseDurationMillis": 0 36 | }, 37 | { 38 | "status": "OTHER", 39 | "name": "Deploy to UAT", 40 | "startTimeMillis": 1610949378152, 41 | "durationMillis": 29, 42 | "pauseDurationMillis": 0 43 | } 44 | ], 45 | "changeSets": [ 46 | { 47 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 48 | "timestamp": 1610944812000, 49 | "date": "2021-01-18 12:40:12 +0800", 50 | "msg": "[Fireman] add check status when deploy to dev" 51 | } 52 | ] 53 | } 54 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/expected/builds-for-jenkins-5.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipelineId": "fake pipeline", 4 | "number": 82, 5 | "duration": 60420, 6 | "timestamp": 1610949317906, 7 | "result": "SUCCESS", 8 | "url": "http://localhost:8001/job/4km-backend/82/", 9 | "stages": [ 10 | { 11 | "status": "SUCCESS", 12 | "name": "Declarative: Checkout SCM", 13 | "startTimeMillis": 1610949320066, 14 | "durationMillis": 3720, 15 | "pauseDurationMillis": 0 16 | }, 17 | { 18 | "status": "OTHER", 19 | "name": "Build Docker image", 20 | "startTimeMillis": 1610949324441, 21 | "durationMillis": 53584, 22 | "pauseDurationMillis": 0 23 | }, 24 | { 25 | "status": "OTHER", 26 | "name": "Push Docker image", 27 | "startTimeMillis": 1610949378045, 28 | "durationMillis": 36, 29 | "pauseDurationMillis": 0 30 | }, 31 | { 32 | "status": "OTHER", 33 | "name": "Deploy to DEV", 34 | "startTimeMillis": 1610949378101, 35 | "durationMillis": 30, 36 | "pauseDurationMillis": 0 37 | }, 38 | { 39 | "status": "OTHER", 40 | "name": "Deploy to UAT", 41 | "startTimeMillis": 1610949378152, 42 | "durationMillis": 29, 43 | "pauseDurationMillis": 0 44 | } 45 | ], 46 | "changeSets": [ 47 | { 48 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 49 | "timestamp": 1610944812000, 50 | "date": "2021-01-18 12:40:12 +0800", 51 | "msg": "[Fireman] add check status when deploy to dev" 52 | } 53 | ] 54 | } 55 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/raw-build-summary-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 3 | "allBuilds": [ 4 | { 5 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 6 | "building": false, 7 | "duration": 60420, 8 | "number": 82, 9 | "result": "ABORTED", 10 | "timestamp": 1610949317906, 11 | "url": "http://localhost:8001/job/4km-backend/82/", 12 | "changeSets":[ 13 | { 14 | "_class": "hudson.plugins.git.GitChangeSetList", 15 | "items": [ 16 | { 17 | "_class": "hudson.plugins.git.GitChangeSet", 18 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 19 | "timestamp": 1610944812000, 20 | "date": "2021-01-18 12:40:12 +0800", 21 | "msg": "[Fireman] add check status when deploy to dev" 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/raw-build-summary-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 3 | "allBuilds": [ 4 | { 5 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 6 | "building": false, 7 | "duration": 60420, 8 | "number": 1, 9 | "result": "ABORTED", 10 | "timestamp": 1610949317906, 11 | "url": "http://localhost:8001/job/4km-backend/1/", 12 | "changeSets":[ 13 | { 14 | "_class": "hudson.plugins.git.GitChangeSetList", 15 | "items": [ 16 | { 17 | "_class": "hudson.plugins.git.GitChangeSet", 18 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 19 | "timestamp": 1610944812000, 20 | "date": "2021-01-18 12:40:12 +0800", 21 | "msg": "[Fireman] add check status when deploy to dev" 22 | } 23 | ] 24 | } 25 | ] 26 | }, 27 | { 28 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 29 | "building": false, 30 | "duration": 60420, 31 | "number": 2, 32 | "result": "ABORTED", 33 | "timestamp": 1610001234567, 34 | "url": "http://localhost:8001/job/4km-backend/1/", 35 | "changeSets":[ 36 | { 37 | "_class": "hudson.plugins.git.GitChangeSetList", 38 | "items": [ 39 | { 40 | "_class": "hudson.plugins.git.GitChangeSet", 41 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 42 | "timestamp": 1610001200567, 43 | "date": "2021-01-18 12:40:12 +0800", 44 | "msg": "[Fireman] add check status when deploy to dev" 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/raw-build-summary-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 3 | "allBuilds": [ 4 | { 5 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 6 | "building": false, 7 | "duration": 60420, 8 | "number": 82, 9 | "result": "FAILURE", 10 | "timestamp": 1610949317906, 11 | "url": "http://localhost:8001/job/4km-backend/82/", 12 | "changeSets":[ 13 | { 14 | "_class": "hudson.plugins.git.GitChangeSetList", 15 | "items": [ 16 | { 17 | "_class": "hudson.plugins.git.GitChangeSet", 18 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 19 | "timestamp": 1610944812000, 20 | "date": "2021-01-18 12:40:12 +0800", 21 | "msg": "[Fireman] add check status when deploy to dev" 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/raw-build-summary-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 3 | "allBuilds": [ 4 | { 5 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 6 | "building": false, 7 | "duration": 60420, 8 | "number": 82, 9 | "result": null, 10 | "timestamp": 1610949317906, 11 | "url": "http://localhost:8001/job/4km-backend/82/", 12 | "changeSets":[ 13 | { 14 | "_class": "hudson.plugins.git.GitChangeSetList", 15 | "items": [ 16 | { 17 | "_class": "hudson.plugins.git.GitChangeSet", 18 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 19 | "timestamp": 1610944812000, 20 | "date": "2021-01-18 12:40:12 +0800", 21 | "msg": "[Fireman] add check status when deploy to dev" 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/test/resources/pipeline/jenkins/raw-build-summary-5.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 3 | "allBuilds": [ 4 | { 5 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 6 | "building": false, 7 | "duration": 60420, 8 | "number": 82, 9 | "result": "SUCCESS", 10 | "timestamp": 1610949317906, 11 | "url": "http://localhost:8001/job/4km-backend/82/", 12 | "changeSets":[ 13 | { 14 | "_class": "hudson.plugins.git.GitChangeSetList", 15 | "items": [ 16 | { 17 | "_class": "hudson.plugins.git.GitChangeSet", 18 | "commitId": "d3de1d66161a3badee06471594a2b9b9e3a129fa", 19 | "timestamp": 1610944812000, 20 | "date": "2021-01-18 12:40:12 +0800", 21 | "msg": "[Fireman] add check status when deploy to dev" 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/test/resources/repository/dashboards-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "projectId", 4 | "name": "4-key", 5 | "pipelines": [ 6 | { 7 | "id": "pipelineId", 8 | "username": "username", 9 | "credential": "fake-credential", 10 | "url": "test.com", 11 | "type": "JENKINS" 12 | } 13 | ] 14 | } 15 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/repository/dashboards-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "fake-project", 4 | "name": "4-key", 5 | "pipelines": [] 6 | } 7 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/repository/dashboards-with-pipelines.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "name": "4-key", 5 | "pipelines": [ 6 | { 7 | "id": "1", 8 | "name": "4km", 9 | "username": "username", 10 | "credential": "fake-credential", 11 | "url": "test.com", 12 | "type": "JENKINS" 13 | }, 14 | { 15 | "id": "2", 16 | "name": "5km", 17 | "username": "username", 18 | "credential": "fake-credential", 19 | "url": "test.com", 20 | "type": "JENKINS" 21 | }, 22 | { 23 | "id": "3", 24 | "name": "6km", 25 | "username": "username", 26 | "credential": "fake-credential", 27 | "url": "test.com", 28 | "type": "JENKINS" 29 | } 30 | ] 31 | } 32 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/repository/dashboards-without-pipelines.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "name": "4-key", 5 | "pipelines": [] 6 | } 7 | ] -------------------------------------------------------------------------------- /backend/src/test/resources/repository/pipelines-in-one-dashboard.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "name": "4km", 5 | "username": "username", 6 | "credential": "fake-credential", 7 | "url": "test.com", 8 | "type": "JENKINS" 9 | }, 10 | { 11 | "id": "2", 12 | "name": "5km", 13 | "username": "username", 14 | "credential": "fake-credential", 15 | "url": "test.com", 16 | "type": "JENKINS" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /ci/.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea 3 | *.iws 4 | *.iml 5 | *.ipr 6 | out/ 7 | !**/src/main/**/out/ 8 | !**/src/test/**/out/ 9 | 10 | ### NetBeans ### 11 | /nbproject/private/ 12 | /nbbuild/ 13 | /dist/ 14 | /nbdist/ 15 | /.nb-gradle/ 16 | 17 | ### VS Code ### 18 | .vscode/ 19 | 20 | ### macOS Temporary Files ### 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /ci/config/mongo/mongo-create-user.js: -------------------------------------------------------------------------------- 1 | use 4-key-metrics; 2 | db.createUser({ user: "4km", pwd: "4000km", roles: [{ role: "readWrite", db: "4-key-metrics" } ] } ); 3 | -------------------------------------------------------------------------------- /ci/config/mongo/mongo-init-replica-set.js: -------------------------------------------------------------------------------- 1 | rs.initiate({'_id': 'rs0', 'members': [{'_id': 0, 'host': '127.0.0.1:27017'}]}); 2 | rs.status(); 3 | -------------------------------------------------------------------------------- /ci/config/mongo/mongo-init.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo "Start checking MongoDB readiness..." 4 | until mongo -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD --eval "print(\"Finally MongoDB is up...\")" 5 | do 6 | echo "Waiting for MongoDB to come up..." 7 | sleep 2 8 | done 9 | echo "Stop checking MongoDB readiness..." 10 | 11 | echo "Start MongoDB initialization" 12 | echo "Step 1: initializing replica set" 13 | mongo -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD < /app/mongo/mongo-init-replica-set.js 14 | echo "Wait 10 seconds for replica set to run" 15 | sleep 10 16 | echo "Step 2: add user" 17 | mongo -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD < /app/mongo/mongo-create-user.js 18 | echo "MongoDB initialization done" 19 | -------------------------------------------------------------------------------- /ci/config/nginx_release.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | worker_processes auto; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | include /etc/nginx/mime.types; 14 | default_type application/octet-stream; 15 | 16 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 17 | '$status $body_bytes_sent "$http_referer" ' 18 | '"$http_user_agent" "$http_x_forwarded_for"'; 19 | 20 | access_log /var/log/nginx/access.log main; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | #gzip on; 26 | 27 | server { 28 | listen 80; 29 | server_name localhost; 30 | 31 | root /usr/share/nginx/html; 32 | 33 | error_page 404 /; 34 | 35 | location /api/ { 36 | proxy_pass http://127.0.0.1:9000; 37 | 38 | # Extends timeout for data sync process 39 | # Should find a better way to sync data more efficiently 40 | proxy_connect_timeout 600; 41 | proxy_send_timeout 600; 42 | proxy_read_timeout 600; 43 | } 44 | 45 | location / { 46 | index index.html; 47 | 48 | add_header Cache-Control no-store; 49 | expires off; 50 | etag off; 51 | } 52 | 53 | location ~* \.(js|css|png|jpg|jpeg|svg|woff|woff2|e0t|ttf|otf)$ { 54 | expires 10d; 55 | etag on; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ci/config/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:mongodb] 5 | command=/bin/bash -c "/usr/local/bin/docker-entrypoint.sh mongod --keyFile /app/mongo/keyfile --bind_ip_all --replSet rs0" 6 | priority=1 7 | startsecs=30 8 | redirect_stderr=false 9 | stdout_logfile=/app/logs/mongod.log 10 | 11 | [program:mongodb-init] 12 | command=/bin/bash -c "/app/mongo/mongo-init.sh" 13 | priority=2 14 | redirect_stderr=false 15 | stdout_logfile=/app/logs/mongod.log 16 | 17 | [program:metrik-service] 18 | command=/bin/bash -c "/app/metrik-service.sh" 19 | priority=10 20 | startsecs=10 21 | redirect_stderr=false 22 | stdout_logfile=/app/logs/4km-service.log 23 | 24 | [program:nginx] 25 | command=/usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf 26 | priority=20 27 | autostart=true 28 | autorestart=true 29 | redirect_stderr=true 30 | stdout_logfile=/app/logs/nginx.log 31 | stderr_logfile=/app/logs/nginx.err.log 32 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | coverage 4 | 5 | test 6 | jest.config.js 7 | 8 | README.md 9 | 10 | .editorconfig 11 | .*rc 12 | .*ignore 13 | .git 14 | 15 | .idea 16 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | tab_width = 2 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2021": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:jest/recommended", 14 | "plugin:prettier/recommended", 15 | "prettier/@typescript-eslint", 16 | "prettier/react" 17 | ], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "jsx": true 22 | }, 23 | "ecmaVersion": 2020, 24 | "sourceType": "module" 25 | }, 26 | "plugins": [ 27 | "react", 28 | "react-hooks", 29 | "@typescript-eslint", 30 | "prettier" 31 | ], 32 | "rules": { 33 | "prettier/prettier": [ 34 | "warn" 35 | ], 36 | "react/display-name": [ 37 | "off" 38 | ], 39 | "react/prop-types": [ 40 | "off" 41 | ] 42 | }, 43 | "settings": { 44 | "react": { 45 | "pragma": "React", 46 | "fragment": "Fragment", 47 | "version": "detect" 48 | } 49 | }, 50 | "overrides": [ 51 | { 52 | "files": [ 53 | "*.js" 54 | ], 55 | "env": { 56 | "commonjs": true 57 | }, 58 | "rules": { 59 | "@typescript-eslint/no-var-requires": "off", 60 | "@typescript-eslint/ban-types": [ 61 | "off", 62 | { 63 | "types": { 64 | "Number": true 65 | } 66 | } 67 | ] 68 | } 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | #ide 5 | .idea 6 | .DS_Store 7 | 8 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "useTabs": true, 4 | "tabWidth": 2, 5 | "singleQuote": false, 6 | "trailingComma": "es5", 7 | "jsxBracketSameLine": true, 8 | "arrowParens": "avoid", 9 | "printWidth": 100 10 | } 11 | -------------------------------------------------------------------------------- /frontend/.stylelintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | node_modules/**/* 3 | **/*.{js,jsx,ts,tsx} -------------------------------------------------------------------------------- /frontend/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-rational-order" 5 | ], 6 | "plugins": [ 7 | "stylelint-declaration-block-no-ignored-properties" 8 | ], 9 | "rules": { 10 | "indentation": "tab", 11 | "plugin/declaration-block-no-ignored-properties": true 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | [![Frontend test](https://github.com/thoughtworks/metrik/actions/workflows/frontend_test.yaml/badge.svg)](https://github.com/thoughtworks/metrik/actions/workflows/frontend_test.yaml) 2 | 3 | 4 |
5 | Table of Contents 6 | 12 |
13 | 14 | 15 | This is the frontend SPA layer of *Metrik*, the 4-key-metrics measurement tool. 16 | 17 | # Tech Stack 18 | * React 19 | * Typescript 20 | * Recharts 21 | * Webpack 22 | * Jest & Testing-Library For React 23 | 24 | # Requirement 25 | Node Version >= 10 26 | 27 | 28 | # Getting Started 29 | ## Run Locally 30 | Checkout the repo to local and go to the project folder: `${REPO_FOLDER}/frontend` 31 | 32 | * Install dependencies 33 | ```bash 34 | npm i 35 | ``` 36 | 37 | * Start up app locally 38 | ```bash 39 | npm run start:local-api 40 | ``` 41 | Then you can access http://localhost:2333 to check web page. 42 | 43 | ## Test the project 44 | * Test 45 | ```bash 46 | npm test 47 | ``` 48 | Then coverage folder is built in `${REPO_FOLDER}/frontend`, which includes all the test coverage reports. 49 | 50 | ## Build bundles 51 | * Build 52 | ```bash 53 | npm run build:prod 54 | ``` 55 | 56 | * Build with Analyzer 57 | ```bash 58 | npm run build:prod:analyze 59 | ``` 60 | Then dist folder is built in `${REPO_FOLDER}/frontend`, which includes all the bundles. 61 | 62 | 63 | # Git Hooks 64 | * use **pre-commit** to do type and code style checking 65 | * use **pre-push** to do unit test and test coverage checking 66 | 67 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | const { isTest } = require("./scripts/constants"); 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | modules: isTest ? "auto" : false, 9 | }, 10 | ], 11 | "@babel/preset-react", 12 | "@babel/preset-typescript", 13 | ["@emotion/babel-preset-css-prop"], 14 | ], 15 | plugins: [ 16 | isTest 17 | ? undefined 18 | : [ 19 | "@babel/plugin-transform-runtime", 20 | { 21 | corejs: 3, 22 | helpers: true, 23 | useESModules: true, 24 | }, 25 | ], 26 | [ 27 | "import", 28 | { 29 | libraryName: "antd", 30 | libraryDirectory: "es", 31 | style: true, 32 | }, 33 | ], 34 | ].filter(Boolean), 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | 4 | transform: { 5 | "^.+\\.[jt]sx?$": "babel-jest", 6 | }, 7 | transformIgnorePatterns: [], 8 | 9 | collectCoverage: true, 10 | coverageDirectory: "coverage", 11 | collectCoverageFrom: [ 12 | "./src/**/*.ts", 13 | "!./src/models/*", 14 | "!./src/clients/*", 15 | "!**/*.d.ts", 16 | "!./src/constants/*", 17 | "!./src/hooks/*", 18 | "!./src/globalStyle.ts", 19 | ], 20 | coverageReporters: ["html", "text", "cobertura"], 21 | coverageThreshold: { 22 | global: { 23 | branches: 80, 24 | functions: 80, 25 | lines: 80, 26 | statements: 80, 27 | }, 28 | }, 29 | 30 | setupFilesAfterEnv: ["/test/setup.ts", "/test/jsdomHelper.ts"], 31 | moduleNameMapper: { 32 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$": "/test/mocks/fileMock.ts", 33 | "\\.(css|less)$": "/test/mocks/styleMock.ts", 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 4 Key Metrics 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/scripts/constants.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const ENTRY_PATH = path.resolve(__dirname, "../src/App.tsx"); 4 | const OUTPUT_PATH = path.resolve(__dirname, "../dist"); 5 | 6 | const PUBLIC_HTML_PATH = path.resolve(__dirname, "../public/index.html"); 7 | const FAVICON_PATH = path.resolve(__dirname, "../src/assets/source/favicon.svg"); 8 | 9 | const isDev = process.env.NODE_ENV === "dev"; 10 | const isTest = process.env.NODE_ENV === "test"; 11 | const enableAnalyzer = !!process.env.ENABLE_ANALYZER; 12 | 13 | module.exports = { 14 | ENTRY_PATH, 15 | OUTPUT_PATH, 16 | PUBLIC_HTML_PATH, 17 | FAVICON_PATH, 18 | isDev, 19 | isTest, 20 | enableAnalyzer, 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/scripts/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const commonConfig = require("./webpack.common"); 3 | const { OUTPUT_PATH } = require("./constants"); 4 | 5 | const devConfig = { 6 | mode: "development", 7 | devtool: "cheap-module-source-map", 8 | devServer: { 9 | allowedHosts: "all", 10 | static: OUTPUT_PATH, 11 | host: "localhost", 12 | port: 2333, 13 | hot: true, 14 | client: { 15 | overlay: true, 16 | }, 17 | open: true, 18 | historyApiFallback: true, 19 | compress: false, 20 | proxy: { 21 | "/api": "http://localhost:9000", 22 | }, 23 | }, 24 | }; 25 | 26 | module.exports = merge(commonConfig, devConfig); 27 | -------------------------------------------------------------------------------- /frontend/scripts/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 6 | const commonConfig = require("./webpack.common"); 7 | const { enableAnalyzer } = require("./constants"); 8 | 9 | const prodConfig = { 10 | mode: "production", 11 | devtool: false, 12 | plugins: [ 13 | new MiniCssExtractPlugin({ 14 | filename: "css/[name].[contenthash:8].css", 15 | }), 16 | enableAnalyzer && new BundleAnalyzerPlugin(), 17 | ].filter(Boolean), 18 | optimization: { 19 | splitChunks: { 20 | chunks: "all", 21 | cacheGroups: { 22 | vendors: { 23 | name: "vendors", 24 | test: /[\\/]node_modules[\\/]/, 25 | priority: -10, 26 | reuseExistingChunk: true, 27 | }, 28 | default: { 29 | minChunks: 2, 30 | priority: -20, 31 | reuseExistingChunk: true, 32 | }, 33 | }, 34 | }, 35 | 36 | minimize: true, 37 | minimizer: [ 38 | new TerserPlugin({ 39 | extractComments: false, 40 | }), 41 | new CssMinimizerPlugin(), 42 | ], 43 | }, 44 | }; 45 | 46 | module.exports = merge(commonConfig, prodConfig); 47 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from "react"; 2 | import ReactDom from "react-dom"; 3 | import { Routes } from "./routes/Routes"; 4 | import { BrowserRouter as Router } from "react-router-dom"; 5 | import Header from "./components/Header"; 6 | import { useRequest } from "./hooks/useRequest"; 7 | import { Global } from "@emotion/react"; 8 | import { getProjectsUsingGet } from "./clients/projectApis"; 9 | import { setResponsive } from "./utils/responsive/responsive"; 10 | import "./assets/fonts/fonts.less"; 11 | import { globalStyles } from "./globalStyle"; 12 | 13 | setResponsive(); 14 | export const App: FC = () => { 15 | const [projects, getProjectsRequest] = useRequest(getProjectsUsingGet); 16 | 17 | useEffect(() => { 18 | getProjectsRequest(undefined); 19 | }, []); 20 | 21 | return projects !== undefined ? ( 22 | <> 23 | 24 | 25 |
26 | 27 | 28 | 29 | ) : null; 30 | }; 31 | 32 | ReactDom.render(, document.getElementById("root")); 33 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/Oswald-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/Oswald-VariableFont_wght.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/static/Oswald-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/static/Oswald-Bold.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/static/Oswald-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/static/Oswald-ExtraLight.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/static/Oswald-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/static/Oswald-Light.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/static/Oswald-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/static/Oswald-Medium.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/static/Oswald-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/static/Oswald-Regular.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Oswald/static/Oswald-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtworks/metrik/def6a52cb7339f6a422451710083177b9c79689a/frontend/src/assets/fonts/Oswald/static/Oswald-SemiBold.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Oswald-Regular"; 3 | src: url("./Oswald/static/Oswald-Regular.ttf") format("truetype"); 4 | } 5 | 6 | @font-face { 7 | font-family: "Oswald-Light"; 8 | src: url("./Oswald/static/Oswald-Light.ttf") format("truetype"); 9 | } 10 | 11 | @font-face { 12 | font-family: "Oswald-Medium"; 13 | src: url("./Oswald/static/Oswald-Medium.ttf") format("truetype"); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/OldLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | function SvgLogo(props: React.SVGProps) { 4 | return ( 5 | 6 | 12 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default SvgLogo; 35 | -------------------------------------------------------------------------------- /frontend/src/assets/source/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/clients/createRequest.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { notification } from "antd"; 3 | 4 | const axiosInstance = axios.create({ 5 | timeout: 300000, // 5 minutes timeout 6 | }); 7 | 8 | export const createRequest = ( 9 | getConfig: (request: TReq) => AxiosRequestConfig 10 | ) => { 11 | const createFn = (request: TReq) => axiosInstance.request(getConfig(request)); 12 | return Object.assign(createFn, { TResp: {} as TResp, TReq: {} as TReq }); 13 | }; 14 | 15 | axiosInstance.interceptors.response.use( 16 | response => response.data, 17 | error => { 18 | const message = error.response?.data?.message || error.message; 19 | notification.error({ 20 | message, 21 | duration: 3, 22 | placement: "topRight", 23 | }); 24 | return Promise.reject(error); 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /frontend/src/clients/metricsApis.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from "./createRequest"; 2 | import { MetricsInfo, MetricsUnit } from "../models/metrics"; 3 | 4 | interface PipelineStageRequest { 5 | pipelineId: string; 6 | stage: string; 7 | } 8 | 9 | export interface MetricsQueryRequest { 10 | pipelineStages: PipelineStageRequest[]; 11 | unit: MetricsUnit; 12 | startTime: number; 13 | endTime: number; 14 | } 15 | 16 | export interface FourKeyMetrics { 17 | changeFailureRate: MetricsInfo; 18 | deploymentFrequency: MetricsInfo; 19 | leadTimeForChange: MetricsInfo; 20 | meanTimeToRestore: MetricsInfo; 21 | } 22 | 23 | export const getFourKeyMetricsUsingPost = createRequest< 24 | { 25 | metricsQuery: MetricsQueryRequest; 26 | }, 27 | FourKeyMetrics 28 | >(({ metricsQuery }) => ({ 29 | method: "POST", 30 | url: `/api/pipeline/metrics`, 31 | data: metricsQuery, 32 | headers: { "Content-Type": "application/json" }, 33 | })); 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/clients/projectApis.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from "./createRequest"; 2 | import { Pipeline } from "./pipelineApis"; 3 | 4 | export const updateProjectNameUsingPut = createRequest<{ 5 | projectId: string; 6 | projectName: string; 7 | }>(({ projectId, projectName }) => ({ 8 | url: `/api/project/${projectId}`, 9 | method: "PUT", 10 | data: { 11 | projectName 12 | }, 13 | headers: { "Content-Type": "application/json" }, 14 | })); 15 | 16 | export const createProjectUsingPost = createRequest< 17 | { 18 | projectName: string; 19 | pipeline: Omit; 20 | }, 21 | BaseProject 22 | >(project => ({ 23 | url: `/api/project`, 24 | method: "POST", 25 | data: project, 26 | headers: { "Content-Type": "application/json" }, 27 | })); 28 | 29 | export const getProjectDetailsUsingGet = createRequest< 30 | { 31 | projectId: string; 32 | }, 33 | Project 34 | >(({ projectId }) => ({ 35 | url: `/api/project/${projectId}`, 36 | method: "GET", 37 | })); 38 | 39 | export const getProjectsUsingGet = createRequest[]>(() => ({ 40 | url: `/api/project`, 41 | method: "GET", 42 | })); 43 | 44 | export const getLastSynchronizationUsingGet = createRequest< 45 | { 46 | projectId: string; 47 | }, 48 | Pick 49 | >(({ projectId }) => ({ 50 | url: `/api/project/${projectId}/synchronization`, 51 | method: "GET", 52 | })); 53 | 54 | export const updateBuildsUsingPost = createRequest< 55 | { 56 | projectId: string; 57 | }, 58 | Pick 59 | >(({ projectId }) => ({ 60 | url: `/api/project/${projectId}/synchronization`, 61 | method: "POST", 62 | headers: { "Content-Type": "application/json" }, 63 | })); 64 | 65 | export interface Project { 66 | id: string; 67 | name: string; 68 | synchronizationTimestamp: number; 69 | pipelines: Pipeline[]; 70 | } 71 | 72 | export type BaseProject = Pick; 73 | -------------------------------------------------------------------------------- /frontend/src/components/AreaChart/AreaChart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Area, AreaChart as ReChartAreaChart, ResponsiveContainer, Tooltip } from "recharts"; 3 | import { CurveType } from "recharts/types/shape/Curve"; 4 | import { AREA_GRADIENT_DEFAULT_COLOR } from "../../constants/styles"; 5 | 6 | interface AreaChartProps extends React.HTMLAttributes { 7 | data: T[]; 8 | dataKey: Exclude; 9 | width: number | string; 10 | height?: number | string; 11 | strokeColor?: string; 12 | strokeWidth?: number; 13 | areaGradientColor?: string; 14 | curveType?: CurveType; 15 | } 16 | const AreaChart = ({ 17 | data, 18 | dataKey, 19 | width = 700, 20 | height = 350, 21 | strokeColor = "#000000", 22 | strokeWidth = 1, 23 | areaGradientColor = AREA_GRADIENT_DEFAULT_COLOR, 24 | curveType = "monotone", 25 | ...restProps 26 | }: AreaChartProps) => { 27 | return ( 28 | <> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | export default AreaChart; 51 | -------------------------------------------------------------------------------- /frontend/src/components/ColourLegend.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Colour, LegendRect } from "./LegendRect"; 3 | import { Col, Row, Typography } from "antd"; 4 | import { GRAY_1 } from "../constants/styles"; 5 | 6 | const { Text, Title } = Typography; 7 | 8 | interface ColourLegendProps { 9 | elite: string; 10 | high: string; 11 | medium: string; 12 | low: string; 13 | } 14 | 15 | const textStyle = { color: GRAY_1, fontSize: 12 }; 16 | 17 | export const ColourLegend: FC = ({ elite, high, medium, low }) => ( 18 |
19 | 20 | How to evaluate it? 21 | 22 | 23 | 24 | 25 | 26 | 27 | {elite} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {high} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {medium} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {low} 52 | 53 | 54 |
55 | ); 56 | -------------------------------------------------------------------------------- /frontend/src/components/EditableText.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Typography } from "antd"; 2 | import React, { FC, useState } from "react"; 3 | import { EditOutlined } from "@ant-design/icons"; 4 | import { BLUE_5 } from "../constants/styles"; 5 | import { trim } from "lodash"; 6 | 7 | const { Title } = Typography; 8 | 9 | interface EditableTextProps { 10 | defaultValue: string; 11 | onEditDone?: (value: string) => void; 12 | } 13 | 14 | export const EditableText: FC = ({ defaultValue, onEditDone }) => { 15 | const [editable, setEditable] = useState(false); 16 | const [value, setValue] = useState(defaultValue); 17 | 18 | const handleEdit = (value?: string) => { 19 | const nextValue = trim(value) || defaultValue; 20 | 21 | setValue(nextValue); 22 | setEditable(false); 23 | onEditDone && onEditDone(nextValue); 24 | }; 25 | 26 | return ( 27 |
28 | {editable ? ( 29 | handleEdit(evt.target.value)} 35 | onKeyDown={e => { 36 | if (e.key === "Enter") { 37 | handleEdit((e.target as HTMLInputElement).value); 38 | } 39 | }} 40 | /> 41 | ) : ( 42 |
{ 45 | setEditable(true); 46 | }}> 47 | 48 | {value} 49 | 50 | 51 |
52 | )} 53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /frontend/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/react"; 2 | import React, { FC } from "react"; 3 | import Logo from "./Logo/Logo"; 4 | import { Link } from "react-router-dom"; 5 | 6 | const headerStyles = css({ 7 | padding: "12px 30px 12px 32px", 8 | boxShadow: "0 2px 4px #f0f1f2", 9 | backgroundColor: "#ffffff", 10 | display: "flex", 11 | alignItems: "center", 12 | }); 13 | 14 | const Header: FC = () => { 15 | return ( 16 | 17 |
18 | 19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Header; 25 | -------------------------------------------------------------------------------- /frontend/src/components/HintIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Tooltip, Typography, Button } from "antd"; 3 | import { InfoCircleOutlined } from "@ant-design/icons/lib/icons"; 4 | import { HINT_ICON_COLOR, OVERLAY_COLOR } from "../constants/styles"; 5 | 6 | interface HintIconProps { 7 | text?: string; 8 | tooltip: string; 9 | } 10 | 11 | const { Text } = Typography; 12 | 13 | const HintIcon: FC = ({ text, tooltip }) => { 14 | return ( 15 | <> 16 | {text ? {text} : null} 17 | 22 | 54 | 55 | 56 | 57 | ); 58 | }} 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default ProjectNameSetup; 65 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/components/Fullscreen/components/MetricsLegend.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Colour, LegendRect } from "../../../../../components/LegendRect"; 3 | import Word from "../../../../../components/Word/Word"; 4 | import { GRAY_1 } from "../../../../../constants/styles"; 5 | import { MetricsLevel } from "../../../../../models/metrics"; 6 | 7 | interface Legend { 8 | text: MetricsLevel; 9 | color: Colour; 10 | } 11 | 12 | const MetricsLegend = () => { 13 | const legendList: Legend[] = [ 14 | { 15 | color: Colour.green, 16 | text: MetricsLevel.ELITE, 17 | }, 18 | { 19 | color: Colour.blue, 20 | text: MetricsLevel.HIGH, 21 | }, 22 | { 23 | color: Colour.orange, 24 | text: MetricsLevel.MEDIUM, 25 | }, 26 | { 27 | color: Colour.red, 28 | text: MetricsLevel.LOW, 29 | }, 30 | ]; 31 | const legendRectStyle = { 32 | display: "block", 33 | marginBottom: "0.4vh", 34 | }; 35 | return ( 36 |
37 |

38 | 39 |

40 | {legendList.map(({ color, text }, index) => ( 41 | 50 | ))} 51 |
52 | ); 53 | }; 54 | export default MetricsLegend; 55 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/components/MetricInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { ColourLegend } from "../../../components/ColourLegend"; 3 | import { metricsStanderMapping, metricsExplanations } from "../../../constants/metrics"; 4 | import { Typography } from "antd"; 5 | import { GRAY_1 } from "../../../constants/styles"; 6 | import { MetricsUnit, MetricType } from "../../../models/metrics"; 7 | 8 | const { Title, Paragraph } = Typography; 9 | 10 | const titleStyle = { color: GRAY_1, fontSize: 14 }; 11 | 12 | export const MetricInfo: FC<{ unit: MetricsUnit; type: MetricType }> = ({ unit, type }) => ( 13 |
14 | 15 | What is it? 16 | 17 | {metricsExplanations[type]} 18 | 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/components/MetricTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { MetricInfo } from "./MetricInfo"; 2 | import { Button, Tooltip } from "antd"; 3 | import { InfoCircleOutlined } from "@ant-design/icons"; 4 | import React, { FC } from "react"; 5 | import { GRAY_13, HINT_ICON_COLOR, OVERLAY_COLOR } from "../../../constants/styles"; 6 | import { MetricsUnit, MetricType } from "../../../models/metrics"; 7 | 8 | export const MetricTooltip: FC<{ unit: MetricsUnit; type: MetricType }> = ({ unit, type }) => ( 9 | }> 15 |