├── .artifacts └── README ├── .dockerignore ├── .editorconfig ├── .env.example ├── .envrc ├── .eslintignore ├── .freight.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── .python-version ├── .snyk ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── babel.config.js ├── bin ├── docker-entrypoint ├── get-node-version ├── get-yarn-version ├── ssh-connect └── zeus ├── bors.toml ├── cloudbuild.yaml ├── config ├── env.js ├── jest │ ├── async.js │ ├── cssTransform.js │ ├── date.js │ ├── enzyme.js │ ├── fileTransform.js │ └── testStubs.js ├── paths.js ├── polyfills.js └── webpack.config.js ├── conftest.py ├── docker-compose.yml ├── kubernetes └── zeus │ ├── .gitignore │ ├── Chart.yaml │ ├── README.md │ ├── app-crd.yaml │ ├── connect-to-postgres │ ├── generate-rabbitmq-manifest │ ├── rabbitmq-1_manifest.yaml │ ├── rabbitmq-1_rbac.yaml │ ├── values.yaml │ └── zeus │ ├── cleanup-evicted-job.yaml │ ├── postgres.yaml │ ├── pubsub.yaml │ ├── redis-master.yaml │ ├── repo-sc.yaml │ ├── standard-sc.yaml │ ├── vcs-server.yaml │ ├── web.yaml │ └── worker.yaml ├── package.json ├── poetry.lock ├── public ├── favicon.ico └── manifest.json ├── pyproject.toml ├── scripts ├── build.js ├── start.js └── test.js ├── setup.cfg ├── templates ├── debug │ └── email.html ├── emails │ ├── base.html │ ├── build-notification.html │ ├── build-notification.txt │ ├── deactivated-repository.html │ └── deactivated-repository.txt └── index.html ├── tests ├── __init__.py ├── fixtures │ ├── sample-checkstyle.xml │ ├── sample-clover.xml │ ├── sample-cobertura.xml │ ├── sample-gotest.json │ ├── sample-jacoco.xml │ ├── sample-pep8.txt │ ├── sample-pylint.txt │ ├── sample-xunit-with-artifacts.xml │ ├── sample-xunit.xml │ ├── sample.diff │ ├── travis-build-commit.json │ ├── travis-build-pull-request.json │ ├── webpack-stats-children.json │ └── webpack-stats.json └── zeus │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── resources │ │ ├── __init__.py │ │ ├── test_artifact_download.py │ │ ├── test_auth_index.py │ │ ├── test_build_artifacts.py │ │ ├── test_build_bundlestats.py │ │ ├── test_build_details.py │ │ ├── test_build_failures.py │ │ ├── test_build_file_coverage.py │ │ ├── test_build_file_coverage_tree.py │ │ ├── test_build_index.py │ │ ├── test_build_jobs.py │ │ ├── test_build_styleviolations.py │ │ ├── test_build_tests.py │ │ ├── test_change_request_details.py │ │ ├── test_change_request_index.py │ │ ├── test_github_organizations.py │ │ ├── test_github_repositories.py │ │ ├── test_hook_details.py │ │ ├── test_install_index.py │ │ ├── test_install_stats.py │ │ ├── test_job_artifacts.py │ │ ├── test_job_details.py │ │ ├── test_job_tests.py │ │ ├── test_repository_branches.py │ │ ├── test_repository_builds.py │ │ ├── test_repository_change_requests.py │ │ ├── test_repository_details.py │ │ ├── test_repository_hooks.py │ │ ├── test_repository_index.py │ │ ├── test_repository_revisions.py │ │ ├── test_repository_stats.py │ │ ├── test_repository_test_details.py │ │ ├── test_repository_test_history.py │ │ ├── test_repository_tests.py │ │ ├── test_repository_testtree.py │ │ ├── test_revision_artifacts.py │ │ ├── test_revision_details.py │ │ ├── test_revision_failures.py │ │ ├── test_revision_tests.py │ │ ├── test_test_details.py │ │ ├── test_user_details.py │ │ └── test_user_emails.py │ ├── schemas │ │ ├── __init__.py │ │ └── test_testcase.py │ └── test_authentication.py │ ├── artifacts │ ├── __init__.py │ ├── test_checkstyle.py │ ├── test_coverage.py │ ├── test_gotest.py │ ├── test_manager.py │ ├── test_pycodestyle.py │ ├── test_pylint.py │ ├── test_webpack.py │ └── test_xunit.py │ ├── db │ └── test_utils.py │ ├── models │ ├── __init__.py │ ├── test_artifact.py │ ├── test_build.py │ ├── test_hook.py │ └── test_repository.py │ ├── notifications │ ├── __init__.py │ └── test_email.py │ ├── providers │ ├── __init__.py │ ├── test_custom.py │ └── travis │ │ ├── __init__.py │ │ ├── test_base.py │ │ └── test_webhook.py │ ├── tasks │ ├── __init__.py │ ├── test_aggregate_job_stats.py │ ├── test_cleanup_artifacts.py │ ├── test_cleanup_builds.py │ ├── test_cleanup_pending_artifacts.py │ ├── test_deactivate_repo.py │ ├── test_delete_repo.py │ ├── test_process_artifact.py │ ├── test_process_pending_artifact.py │ ├── test_process_travis_webhook.py │ ├── test_resolve_ref.py │ ├── test_send_build_notifications.py │ ├── test_sync_github_access.py │ └── test_testcase_rollups.py │ ├── utils │ ├── __init__.py │ ├── test_auth.py │ ├── test_builds.py │ ├── test_metrics.py │ ├── test_revisions.py │ ├── test_testresult.py │ ├── test_trees.py │ └── test_upserts.py │ ├── vcs │ ├── __init__.py │ ├── backends │ │ └── test_git.py │ ├── conftest.py │ ├── test_api.py │ ├── test_client.py │ ├── test_server.py │ └── test_utils.py │ └── web │ ├── __init__.py │ ├── hooks │ ├── __init__.py │ ├── test_build_hook.py │ ├── test_job_artifacts_hook.py │ └── test_job_hook.py │ ├── test_auth_github.py │ ├── test_health_check.py │ └── test_index.py ├── webapp ├── actions │ ├── auth.jsx │ ├── builds.jsx │ ├── changeRequests.jsx │ ├── indicators.jsx │ ├── repos.jsx │ ├── revisions.jsx │ └── stream.jsx ├── api.jsx ├── assets │ ├── IconCircleCheck.jsx │ ├── IconCircleCross.jsx │ ├── IconClock.jsx │ ├── IconClock.svg │ ├── Logo.jsx │ └── screenshots │ │ └── BuildDetails.png ├── components │ ├── AggregateFailureList.jsx │ ├── AggregateTestList.jsx │ ├── ArtifactsList.jsx │ ├── AsyncComponent.jsx │ ├── AsyncPage.jsx │ ├── Badge.jsx │ ├── Breadcrumbs.jsx │ ├── BuildCoverage.jsx │ ├── BuildDetailsBase.jsx │ ├── BuildLink.jsx │ ├── BuildList.jsx │ ├── BuildListItem.jsx │ ├── BuildOverviewBase.jsx │ ├── BundleList.jsx │ ├── Button.jsx │ ├── ChangeRequestList.jsx │ ├── ChangeRequestListItem.jsx │ ├── Collapsable.jsx │ ├── Container.jsx │ ├── Content.jsx │ ├── CoverageSummary.jsx │ ├── DefinitionList.jsx │ ├── Diff.jsx │ ├── Duration.jsx │ ├── ErrorBoundary.jsx │ ├── Fieldset.jsx │ ├── FileSize.jsx │ ├── Footer.jsx │ ├── FormActions.jsx │ ├── GitHubLoginButton.jsx │ ├── Header.jsx │ ├── HorizontalHeader.jsx │ ├── Icon.jsx │ ├── IdentityNeedsUpgradeError.jsx │ ├── Indicators.jsx │ ├── Input.jsx │ ├── InternalError.jsx │ ├── JobList.jsx │ ├── Label.jsx │ ├── Layout.jsx │ ├── ListItemLink.jsx │ ├── ListLink.jsx │ ├── Login.jsx │ ├── Modal.jsx │ ├── Nav.jsx │ ├── NavHeading.jsx │ ├── NetworkError.jsx │ ├── NotFoundError.jsx │ ├── ObjectAuthor.jsx │ ├── ObjectCoverage.jsx │ ├── ObjectDuration.jsx │ ├── ObjectResult.jsx │ ├── PageLoading.jsx │ ├── PageLoadingIndicator.jsx │ ├── Paginator.jsx │ ├── Panel.jsx │ ├── RepositoryContent.jsx │ ├── RepositoryHeader.jsx │ ├── RepositoryNav.jsx │ ├── RepositoryNavItem.jsx │ ├── ResultGrid.jsx │ ├── ResultGridRow.jsx │ ├── RevisionList.jsx │ ├── RevisionListItem.jsx │ ├── ScrollView.jsx │ ├── Section.jsx │ ├── SectionHeading.jsx │ ├── SectionSubheading.jsx │ ├── Select.jsx │ ├── StyleViolationList.jsx │ ├── SyntaxHighlight.jsx │ ├── TabbedNav.jsx │ ├── TabbedNavItem.jsx │ ├── TestChart.jsx │ ├── TestDetails.jsx │ ├── TestList.jsx │ ├── TimeSince.jsx │ ├── ToastIndicator.jsx │ ├── Tooltip.jsx │ ├── Tree.jsx │ └── __tests__ │ │ ├── BuildCoverage.spec.jsx │ │ ├── BuildList.spec.jsx │ │ ├── Collapsable.spec.jsx │ │ ├── CoverageSummary.spec.jsx │ │ ├── Diff.spec.jsx │ │ ├── FileSize.spec.jsx │ │ ├── ObjectAuthor.spec.jsx │ │ ├── ObjectDuration.spec.jsx │ │ ├── ObjectResult.spec.jsx │ │ ├── RevisionList.spec.jsx │ │ ├── TimeSince.spec.jsx │ │ └── __snapshots__ │ │ ├── BuildCoverage.spec.jsx.snap │ │ ├── BuildList.spec.jsx.snap │ │ ├── Collapsable.spec.jsx.snap │ │ ├── CoverageSummary.spec.jsx.snap │ │ ├── Diff.spec.jsx.snap │ │ ├── FileSize.spec.jsx.snap │ │ ├── ObjectAuthor.spec.jsx.snap │ │ ├── ObjectDuration.spec.jsx.snap │ │ ├── ObjectResult.spec.jsx.snap │ │ ├── RevisionList.spec.jsx.snap │ │ └── TimeSince.spec.jsx.snap ├── decorators │ └── stream.jsx ├── errors.jsx ├── index.jsx ├── middleware │ ├── sentry.jsx │ └── stream.jsx ├── pages │ ├── AccountSettings.jsx │ ├── App.jsx │ ├── BuildArtifacts.jsx │ ├── BuildCoverage.jsx │ ├── BuildDetails.jsx │ ├── BuildDiff.jsx │ ├── BuildOverview.jsx │ ├── BuildStyleViolationList.jsx │ ├── BuildTestList.jsx │ ├── Dashboard.jsx │ ├── DashboardOrWelcome.jsx │ ├── GitHubRepositoryList.jsx │ ├── Install.jsx │ ├── OwnerDetails.jsx │ ├── RepositoryBuildList.jsx │ ├── RepositoryChangeRequestList.jsx │ ├── RepositoryDetails.jsx │ ├── RepositoryFileCoverage.jsx │ ├── RepositoryHookCreate.jsx │ ├── RepositoryHookDetails.jsx │ ├── RepositoryHooks.jsx │ ├── RepositoryOverview.jsx │ ├── RepositoryReportsLayout.jsx │ ├── RepositoryRevisionList.jsx │ ├── RepositorySettings.jsx │ ├── RepositorySettingsLayout.jsx │ ├── RepositoryStats.jsx │ ├── RepositoryTestList.jsx │ ├── RepositoryTestTree.jsx │ ├── RepositoryTests.jsx │ ├── RevisionArtifacts.jsx │ ├── RevisionCoverage.jsx │ ├── RevisionDetails.jsx │ ├── RevisionDiff.jsx │ ├── RevisionOverview.jsx │ ├── RevisionStyleViolationList.jsx │ ├── RevisionTestList.jsx │ ├── Settings.jsx │ ├── TestDetails.jsx │ ├── TokenSettings.jsx │ ├── UserBuildList.jsx │ ├── Welcome.jsx │ └── __tests__ │ │ ├── App.spec.jsx │ │ ├── BuildArtifacts.spec.jsx │ │ ├── BuildCoverage.spec.jsx │ │ ├── BuildDetails.spec.jsx │ │ ├── BuildOverview.spec.jsx │ │ ├── Dashboard.spec.jsx │ │ ├── RepositoryDetails.spec.jsx │ │ ├── RepositoryHookCreate.spec.jsx │ │ ├── RepositoryOverview.spec.jsx │ │ ├── UserBuildList.spec.jsx │ │ └── __snapshots__ │ │ ├── App.spec.jsx.snap │ │ ├── BuildArtifacts.spec.jsx.snap │ │ ├── BuildCoverage.spec.jsx.snap │ │ ├── BuildDetails.spec.jsx.snap │ │ ├── BuildOverview.spec.jsx.snap │ │ ├── Dashboard.spec.jsx.snap │ │ ├── RepositoryDetails.spec.jsx.snap │ │ ├── RepositoryHookCreate.spec.jsx.snap │ │ ├── RepositoryOverview.spec.jsx.snap │ │ └── UserBuildList.spec.jsx.snap ├── reducers │ ├── auth.jsx │ ├── builds.jsx │ ├── changeRequests.jsx │ ├── index.jsx │ ├── indicators.jsx │ ├── repos.jsx │ ├── revisions.jsx │ └── stream.jsx ├── routes.jsx ├── store.jsx ├── types.jsx └── utils │ ├── __tests__ │ └── duration.spec.jsx │ ├── duration.jsx │ ├── fileSize.jsx │ ├── media.jsx │ └── requireAuth.jsx ├── webpack.config.js ├── yarn.lock └── zeus ├── __init__.py ├── api ├── __init__.py ├── authentication.py ├── client.py ├── controller.py ├── resources │ ├── __init__.py │ ├── artifact_download.py │ ├── auth_index.py │ ├── base.py │ ├── base_artifact.py │ ├── base_build.py │ ├── base_change_request.py │ ├── base_hook.py │ ├── base_job.py │ ├── base_repository.py │ ├── base_revision.py │ ├── build_artifacts.py │ ├── build_bundlestats.py │ ├── build_details.py │ ├── build_diff.py │ ├── build_failures.py │ ├── build_file_coverage.py │ ├── build_file_coverage_tree.py │ ├── build_index.py │ ├── build_jobs.py │ ├── build_styleviolations.py │ ├── build_tests.py │ ├── catchall.py │ ├── change_request_details.py │ ├── change_request_index.py │ ├── github_organizations.py │ ├── github_repositories.py │ ├── hook_details.py │ ├── index.py │ ├── install_index.py │ ├── install_stats.py │ ├── job_artifacts.py │ ├── job_details.py │ ├── job_tests.py │ ├── repository_branches.py │ ├── repository_builds.py │ ├── repository_change_requests.py │ ├── repository_details.py │ ├── repository_file_coverage_tree.py │ ├── repository_hooks.py │ ├── repository_index.py │ ├── repository_revisions.py │ ├── repository_stats.py │ ├── repository_test_details.py │ ├── repository_test_history.py │ ├── repository_tests.py │ ├── repository_testtree.py │ ├── revision_artifacts.py │ ├── revision_bundlestats.py │ ├── revision_details.py │ ├── revision_diff.py │ ├── revision_failures.py │ ├── revision_file_coverage.py │ ├── revision_file_coverage_tree.py │ ├── revision_jobs.py │ ├── revision_styleviolations.py │ ├── revision_tests.py │ ├── test_details.py │ ├── user_details.py │ ├── user_emails.py │ └── user_token.py ├── schemas │ ├── __init__.py │ ├── artifact.py │ ├── author.py │ ├── build.py │ ├── bundlestat.py │ ├── change_request.py │ ├── email.py │ ├── failurereason.py │ ├── fields │ │ ├── __init__.py │ │ ├── enum.py │ │ ├── file.py │ │ ├── permission.py │ │ ├── result.py │ │ ├── revision.py │ │ ├── severity.py │ │ └── status.py │ ├── filecoverage.py │ ├── hook.py │ ├── identity.py │ ├── job.py │ ├── pending_artifact.py │ ├── repository.py │ ├── revision.py │ ├── stats.py │ ├── styleviolation.py │ ├── testcase.py │ ├── testcase_rollup.py │ ├── token.py │ └── user.py └── utils │ ├── __init__.py │ ├── stats.py │ └── upserts.py ├── app.py ├── artifacts ├── __init__.py ├── base.py ├── checkstyle.py ├── coverage.py ├── gotest.py ├── manager.py ├── pycodestyle.py ├── pylint.py ├── webpack.py └── xunit.py ├── auth.py ├── cli ├── __init__.py ├── auth.py ├── base.py ├── cleanup.py ├── devserver.py ├── hooks.py ├── init.py ├── mocks.py ├── pubsub.py ├── repos.py ├── shell.py ├── ssh_connect.py ├── vcs_server.py ├── web.py └── worker.py ├── config.py ├── constants.py ├── db ├── __init__.py ├── func.py ├── mixins.py ├── types │ ├── __init__.py │ ├── enum.py │ ├── file.py │ ├── guid.py │ └── json.py └── utils.py ├── exceptions.py ├── factories ├── __init__.py ├── api_token.py ├── artifact.py ├── author.py ├── base.py ├── build.py ├── bundlestat.py ├── change_request.py ├── email.py ├── failurereason.py ├── filecoverage.py ├── hook.py ├── identity.py ├── job.py ├── pending_artifact.py ├── repository.py ├── revision.py ├── styleviolation.py ├── testcase.py ├── testcase_rollup.py ├── types │ ├── __init__.py │ └── guid.py └── user.py ├── migrations ├── 059fd6da96ee_cleanup_repo_access.py ├── 070ac78ddbcb_optional_label.py ├── 0e26830acd30_refactor_bundlestats.py ├── 0f81e9efc84a_add_pending_artifact.py ├── 133fc1714306_fix_missing_metadata.py ├── 14f53101b654_build_failures_build_only.py ├── 15d0e6fc9b97_add_testcase_created_index.py ├── 16414a5b4ed9_backfill_authors.py ├── 1782e8a9f689_scheduled_repo_updates.py ├── 1cb2c54f3831_add_repo_role.py ├── 1fd74fb6ef0a_repo_owners.py ├── 2174d4350f40_multiple_revision_authors.py ├── 240364a078d9_initial.py ├── 33da83c61635_optimize_origin_index.py ├── 340d5cc7e806_index_artifacts.py ├── 355fb6ea34f8_job_allow_failures.py ├── 392eb568af84_index_finished_jobs.py ├── 404ac069de83_fix_source_constraint.py ├── 426b74e836df_migrate_sources.py ├── 483a30c028ec_remove_source_usage.py ├── 495c51627ec0_backfill_testcase_rollup.py ├── 4b3c2ca23454_identity_scopes.py ├── 523842e356fa_add_build_author.py ├── 52a5d85ba249_backfill_cr_multi_author.py ├── 53c5cd5b170f_artifact_status.py ├── 54bbb66a65a6_fix_bad_authors.py ├── 56684708bb21_multiple_build_authors.py ├── 61a1763b9c8d_reindex_outcomes.py ├── 694bcef51b94_add_change_request.py ├── 6afba33703f5_track_hooks.py ├── 6e7a43dc7b0e_missing_indexes.py ├── 70383b887d4a_cr_multi_author.py ├── 708db2afdda5_fix_source_constraint.py ├── 71a34df585ed_migrate_coverage.py ├── 83dc0a466da2_allow_arbitrary_artifact_types.py ├── 8536b0fcf0a2_add_refs.py ├── 87cbddd5b946_add_artifact_dates.py ├── 890ddf764ed7_backfill_job_updated.py ├── 89a25650d379_repo_provider.py ├── 89a8aa1c611c_add_webpack_stats.py ├── 8bff5351578a_urls.py ├── 8e598bda7fda_public_repository.py ├── 8ee8825cd590_backfill_failure_build.py ├── 9096cdb5c97e_add_testcase_meta.py ├── 9227d42a8935_failure_reasons.py ├── 9418cac1c4ee_styleviolation.py ├── 9d374079e8fc_hook_data.py ├── 9dbc97018a55_migrate_github.py ├── a456f2c3a68d_build_labels.py ├── a8bd86b031b4_add_testcase_date_created.py ├── af3f4bdc27d1_cover_test_history.py ├── b3eb342cfd7e_user_and_repository_tokens.py ├── bc3f73aeee99_remove_bundles.py ├── bfe3af4f7eae_job_date_updated.py ├── c1d328364626_fix_schemas.py ├── c257bd5a6236_email_verified.py ├── cd1324dcb6ba_remove_unused_index.py ├── d05e8a773f11_add_testcase_rollup.py ├── d84e557ec8f6_refactor_bundles.py ├── de49ae79ce38_bundle_job_id_indexes.py ├── e2f926a2b0fe_add_user_date_active.py ├── e373a7bffa18_unique_build_failures.py ├── e688aaea28d2_backfill_build_author.py ├── f78a2be4ddf9_rename_hook_data.py ├── f7c8c10f5aea_rethink_authors.py ├── f8013173ef21_prefix_provider.py ├── f810bb64d19b_email.py ├── f8851082b9d9_remove_unused_tables.py ├── fe3baeb0605e_merge_heads.py └── script.py.mako ├── models ├── __init__.py ├── api_token.py ├── api_token_repository_access.py ├── artifact.py ├── author.py ├── build.py ├── bundlestat.py ├── change_request.py ├── email.py ├── failurereason.py ├── filecoverage.py ├── hook.py ├── identity.py ├── itemoption.py ├── itemsequence.py ├── itemstat.py ├── job.py ├── pending_artifact.py ├── repository.py ├── repository_access.py ├── repository_api_token.py ├── revision.py ├── styleviolation.py ├── testcase.py ├── testcase_meta.py ├── testcase_rollup.py ├── user.py └── user_api_token.py ├── notifications ├── __init__.py └── email.py ├── providers ├── __init__.py ├── base.py ├── custom.py └── travis │ ├── __init__.py │ ├── base.py │ └── webhook.py ├── pubsub ├── __init__.py ├── server.py └── utils.py ├── storage ├── __init__.py ├── base.py ├── gcs.py └── mock.py ├── tasks ├── __init__.py ├── aggregate_job_stats.py ├── cleanup_artifacts.py ├── cleanup_builds.py ├── cleanup_pending_artifacts.py ├── deactivate_repo.py ├── delete_repo.py ├── process_artifact.py ├── process_pending_artifact.py ├── process_travis_webhook.py ├── resolve_ref.py ├── send_build_notifications.py ├── sync_github_access.py └── testcase_rollups.py ├── testutils ├── __init__.py ├── client.py ├── fixtures.py └── pytest.py ├── utils ├── __init__.py ├── aggregation.py ├── artifacts.py ├── asyncio.py ├── builds.py ├── celery.py ├── diff_parser.py ├── email.py ├── functional.py ├── github.py ├── http.py ├── imports.py ├── metrics.py ├── nplusone.py ├── redis.py ├── revisions.py ├── sentry.py ├── ssh.py ├── ssl.py ├── testresult.py ├── text.py ├── timezone.py └── trees.py ├── vcs ├── __init__.py ├── api.py ├── asserts.py ├── backends │ ├── __init__.py │ ├── base.py │ └── git.py ├── client.py ├── providers │ ├── __init__.py │ ├── base.py │ └── github.py ├── server.py └── utils.py └── web ├── __init__.py ├── debug ├── __init__.py └── notification.py ├── hooks ├── __init__.py ├── base.py ├── build.py ├── change_request.py ├── job.py └── job_artifacts.py └── views ├── __init__.py ├── auth_github.py └── index.py /.artifacts/README: -------------------------------------------------------------------------------- 1 | This folder captures artifacts generated from .pre-commit, such as lint violations. It is primarily used for continuous integration services. 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .envrc 4 | /.git 5 | /.venv 6 | /.kubernetes/zeus 7 | /node_modules 8 | /coverage 9 | /htmlcov 10 | /dist 11 | /build 12 | /static 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.{py}] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.{js,jsx,json,css,scss,less,yml}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID=8292709cc3067652ad9c 2 | 3 | GITHUB_CLIENT_SECRET= 4 | 5 | SECRET_KEY=secret 6 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | layout_poetry() { 2 | if [[ ! -f pyproject.toml ]]; then 3 | log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.' 4 | exit 2 5 | fi 6 | 7 | local VENV=$( poetry show -v|grep "Using virtualenv:"|cut -f 3 -d " " 2>/dev/null) 8 | export VIRTUAL_ENV=$VENV 9 | PATH_add "$VIRTUAL_ENV/bin" 10 | } 11 | 12 | set -e 13 | 14 | # check if python version is set in current dir 15 | if [ -f ".python-version" ] ; then 16 | if [ ! -d ".venv" ] ; then 17 | echo "Installing virtualenv for $(python -V)" 18 | python -m venv .venv 19 | fi 20 | echo "Activating $(python -V) virtualenv" 21 | source .venv/bin/activate 22 | fi 23 | 24 | layout node 25 | layout_poetry 26 | 27 | # load local environment variables 28 | if [ -f ".env" ] ; then 29 | dotenv .env 30 | fi 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | /node_modules/ 4 | /config/ 5 | /scripts/ 6 | babel.config.js 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | /htmlcov 9 | 10 | # production 11 | /dist 12 | /build 13 | /static 14 | 15 | # misc 16 | .coverage* 17 | *.coverage.xml 18 | coverage.xml 19 | *.junit.xml 20 | junit.xml 21 | .cache/ 22 | __pycache__/ 23 | .mypy_cache/ 24 | .DS_Store 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | celerybeat-schedule 31 | 32 | *.pyc 33 | *.egg-info 34 | 35 | # poetry generates a setup.py 36 | /setup.py 37 | 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | 42 | # vscode 43 | .vscode/tags 44 | 45 | /.artifacts 46 | /.env 47 | /.venv 48 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.snap 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "jsxBracketSameLine": true, 5 | "printWidth": 90 6 | } 7 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8.1 2 | 2.7.14 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dzannotti.vscode-babel-coloring", 6 | "esbenp.prettier-vscode", 7 | "ms-python.python", 8 | "dbaeumer.vscode-eslint", 9 | "lextudio.restructuredtext", 10 | "PeterJausovec.vscode-docker", 11 | "DavidAnson.vscode-markdownlint", 12 | "bungcip.better-toml" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check if we're trying to execute a zeus bin 6 | if [ -f "/usr/src/zeus/bin/$1" ]; then 7 | if [ "$(id -u)" = '0' ]; then 8 | mkdir -p "$REPO_ROOT" "$WORKSPACE_ROOT" 9 | chown zeus "$REPO_ROOT" "$WORKSPACE_ROOT" 10 | exec gosu zeus "$@" 11 | fi 12 | fi 13 | 14 | exec "$@" 15 | -------------------------------------------------------------------------------- /bin/get-node-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | 5 | with open("package.json") as fp: 6 | data = json.load(fp) 7 | 8 | print(data["volta"]["node"]) 9 | -------------------------------------------------------------------------------- /bin/get-yarn-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | 5 | with open("package.json") as fp: 6 | data = json.load(fp) 7 | 8 | print(data["volta"]["yarn"]) 9 | -------------------------------------------------------------------------------- /bin/ssh-connect: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | SUPPRESS_LOGGING=1 6 | 7 | ${DIR}/zeus ssh-connect $@ 8 | -------------------------------------------------------------------------------- /bin/zeus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from zeus.cli import main 4 | 5 | main() 6 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "continuous-integration/travis-ci/push", 3 | ] 4 | 5 | delete_merged_branches = true 6 | use_squash_merge = true 7 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | args: ['pull', 'gcr.io/$PROJECT_ID/zeus'] 4 | - name: 'gcr.io/cloud-builders/docker' 5 | args: [ 6 | 'build', 7 | '--cache-from', 'gcr.io/$PROJECT_ID/zeus:latest', 8 | '--build-arg', 'BUILD_REVISION=$COMMIT_SHA', 9 | '-t', 'gcr.io/$PROJECT_ID/zeus:$COMMIT_SHA', 10 | '-t', 'gcr.io/$PROJECT_ID/zeus:latest', 11 | '.' 12 | ] 13 | images: 14 | - 'gcr.io/$PROJECT_ID/zeus:$COMMIT_SHA' 15 | - 'gcr.io/$PROJECT_ID/zeus:latest' 16 | timeout: 3600s 17 | options: 18 | # Run on bigger machines because npm and stuff is slow 19 | machineType: 'N1_HIGHCPU_8' 20 | -------------------------------------------------------------------------------- /config/jest/async.js: -------------------------------------------------------------------------------- 1 | import xhrmock from 'xhr-mock'; 2 | 3 | // This is so we can use async/await in tests instead of wrapping with `setTimeout` 4 | window.tick = () => new Promise(resolve => setTimeout(resolve)); 5 | 6 | xhrmock.error(error => { 7 | console.error(error.err, {url: error.req.url()}); 8 | }); 9 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/date.js: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate'; 2 | 3 | const constantDate = new Date('2017-10-17T04:41:20Z'); //National Pasta Day 4 | MockDate.set(constantDate); 5 | -------------------------------------------------------------------------------- /config/jest/enzyme.js: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({adapter: new Adapter()}); 5 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | global.requestAnimationFrame = function(callback) { 19 | setTimeout(callback, 0); 20 | }; 21 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from datetime import datetime, timezone 4 | from unittest.mock import patch 5 | 6 | pytest_plugins = ["zeus.testutils.pytest", "zeus.testutils.fixtures"] 7 | 8 | 9 | @pytest.fixture(scope="session", autouse=True) 10 | def mock_datetime(): 11 | fixed_now = datetime(2020, 1, 2, 3, 4, 5, tzinfo=timezone.utc) 12 | cm = patch("zeus.utils.timezone.now", return_value=fixed_now) 13 | cm.__enter__() 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "8080:8080" 7 | depends_on: 8 | - db 9 | - redis 10 | env_file: .env 11 | environment: 12 | - SECRET_KEY=unsafe_secret 13 | - REDIS_URL=redis://redis/0 14 | - SQLALCHEMY_DATABASE_URI=postgresql://postgres@db/postgres 15 | db: 16 | image: postgres:latest 17 | ports: 18 | - "5432:5432" 19 | environment: 20 | - POSTGRES_USER=postgres 21 | - POSTGRES_DB=postgres 22 | redis: 23 | image: redis:alpine 24 | ports: 25 | - "6379:6379" 26 | -------------------------------------------------------------------------------- /kubernetes/zeus/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # local venv 7 | /venv 8 | 9 | # testing 10 | /coverage 11 | /htmlcov 12 | 13 | # production 14 | /dist 15 | /build 16 | /static 17 | 18 | # misc 19 | .coverage* 20 | coverage.xml 21 | .cache/ 22 | .mypy_cache/ 23 | .DS_Store 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | celerybeat-schedule 30 | 31 | *.pyc 32 | *.egg-info 33 | 34 | npm-debug.log* 35 | yarn-debug.log* 36 | yarn-error.log* 37 | 38 | # vscode 39 | .vscode/tags 40 | 41 | /.env 42 | /charts/ 43 | -------------------------------------------------------------------------------- /kubernetes/zeus/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: zeus-gcp 2 | version: 0.1.0 3 | description: Zeus CI 4 | home: https://zeus.ci 5 | sources: 6 | - https://github.com/getsentry/zeus-gcp 7 | maintainers: # (optional) 8 | - name: David Cramer 9 | email: dcramer@gmail.com 10 | -------------------------------------------------------------------------------- /kubernetes/zeus/connect-to-postgres: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | SECRET_NAME=cloudsql-db-credentials 4 | 5 | INSTANCE=$(gcloud sql instances list --format "get(name)") 6 | 7 | USERNAME=$(kubectl get secret $SECRET_NAME -o jsonpath="{.data.username}" | base64 --decode) 8 | PASSWORD=$(kubectl get secret $SECRET_NAME -o jsonpath="{.data.password}" | base64 --decode) 9 | 10 | 11 | echo "When prompted, use this password: ${PASSWORD}" 12 | gcloud sql connect $INSTANCE --user=$USERNAME 13 | -------------------------------------------------------------------------------- /kubernetes/zeus/rabbitmq-1_rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: rabbitmq-1-rabbitmq-sa 5 | labels: 6 | app.kubernetes.io/name: rabbitmq-1 7 | app.kubernetes.io/component: rabbitmq-server 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: Role 11 | metadata: 12 | name: rabbitmq-1-rabbitmq-endpoint-reader 13 | labels: 14 | app.kubernetes.io/name: rabbitmq-1 15 | app.kubernetes.io/component: rabbitmq-server 16 | rules: 17 | - apiGroups: [""] 18 | resources: ["endpoints"] 19 | verbs: ["get"] 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: RoleBinding 23 | metadata: 24 | name: rabbitmq-1-rabbitmq-endpoint-reader 25 | labels: 26 | app.kubernetes.io/name: rabbitmq-1 27 | app.kubernetes.io/component: rabbitmq-server 28 | subjects: 29 | - kind: ServiceAccount 30 | name: rabbitmq-1-rabbitmq-sa 31 | roleRef: 32 | apiGroup: rbac.authorization.k8s.io 33 | kind: Role 34 | name: rabbitmq-1-rabbitmq-endpoint-reader 35 | -------------------------------------------------------------------------------- /kubernetes/zeus/values.yaml: -------------------------------------------------------------------------------- 1 | prometheus: 2 | server: 3 | ingress: 4 | ## If true, Prometheus server Ingress will be created 5 | ## 6 | enabled: true 7 | 8 | ## Prometheus server Ingress hostnames 9 | ## Must be provided if Ingress is enabled 10 | ## 11 | hosts: 12 | - prometheus.zeus.ci 13 | 14 | ## Prometheus server Ingress TLS configuration 15 | ## Secrets must be manually created in the namespace 16 | ## 17 | tls: 18 | - secretName: prometheus-server-tls 19 | hosts: 20 | - prometheus.zeus.ci 21 | -------------------------------------------------------------------------------- /kubernetes/zeus/zeus/cleanup-evicted-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: cleanup-evicted-pods 5 | spec: 6 | schedule: "*/30 * * * *" 7 | jobTemplate: 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kubectl-runner 13 | image: wernight/kubectl 14 | command: ["sh", "-c", "kubectl get pods | grep Evicted | awk '{print $1}' | xargs kubectl delete pod"] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /kubernetes/zeus/zeus/repo-sc.yaml: -------------------------------------------------------------------------------- 1 | kind: StorageClass 2 | apiVersion: storage.k8s.io/v1 3 | metadata: 4 | name: standard 5 | provisioner: kubernetes.io/gce-pd 6 | parameters: 7 | type: pd-standard 8 | zone: us-central1-b 9 | -------------------------------------------------------------------------------- /kubernetes/zeus/zeus/standard-sc.yaml: -------------------------------------------------------------------------------- 1 | kind: StorageClass 2 | apiVersion: storage.k8s.io/v1 3 | metadata: 4 | name: standard 5 | provisioner: kubernetes.io/gce-pd 6 | parameters: 7 | type: pd-standard 8 | zone: us-central1-b 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Zeus", 3 | "name": "Zeus", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | process.env.TZ = 'America/New_York'; 8 | 9 | // Makes the script crash on unhandled rejections instead of silently 10 | // ignoring them. In the future, promise rejections that are not handled will 11 | // terminate the Node.js process with a non-zero exit code. 12 | process.on('unhandledRejection', err => { 13 | throw err; 14 | }); 15 | 16 | // Ensure environment variables are read. 17 | require('../config/env'); 18 | 19 | const jest = require('jest'); 20 | const argv = process.argv.slice(2); 21 | 22 | // Watch unless on CI or in coverage mode 23 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 24 | argv.push('--watch'); 25 | } 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /templates/emails/deactivated-repository.html: -------------------------------------------------------------------------------- 1 | {% extends "emails/base.html" %} 2 | 3 | {% block body %} 4 |

Your repository ({{ repo.owner_name }}/{{ repo.name }}) was deactivated in Zeus.

5 |

The reason for the deactivation given:

6 |

{{ reason }}

7 |

To fix this problem, please reconnect the repository.

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/emails/deactivated-repository.txt: -------------------------------------------------------------------------------- 1 | ------------------------------------------ 2 | Repository Disabled 3 | ------------------------------------------ 4 | 5 | Your repository ({{ repo.owner_name }}/{{ repo.name }}) was deactivated in Zeus. 6 | 7 | The reason for the deactivation given: 8 | 9 | {{ reason }} 10 | 11 | To fix this problem, please visit reconnect the repository: 12 | 13 | {{ settings_url }} 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/sample-checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/sample-pep8.txt: -------------------------------------------------------------------------------- 1 | optparse.py:69:11: E401 multiple imports on one line 2 | optparse.py:77:1: W302 expected 2 blank lines, found 1 3 | -------------------------------------------------------------------------------- /tests/fixtures/sample-pylint.txt: -------------------------------------------------------------------------------- 1 | ************* Module zeus 2 | zeus/bar.py:1: [C0111(missing-docstring), ] Missing module docstring 3 | ************* Module zeus.auth 4 | zeus/foo.py:20: [E0326(bad-whitespace), ] Exactly one space required around keyword argument assignment 5 | def __init__(self, repository_ids: Optional[str]=None): 6 | ^ 7 | zeus/foo.py:200: [W0301(line-too-long), ] Line too long (112/100) 8 | -------------------------------------------------------------------------------- /tests/fixtures/sample-xunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/test_report.py:1: in <module> 5 | > import mock 6 | E ImportError: No module named mock 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/zeus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/__init__.py -------------------------------------------------------------------------------- /tests/zeus/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/api/__init__.py -------------------------------------------------------------------------------- /tests/zeus/api/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/api/resources/__init__.py -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_artifact_download.py: -------------------------------------------------------------------------------- 1 | def test_artifact_download( 2 | client, 3 | default_login, 4 | default_repo, 5 | default_build, 6 | default_job, 7 | default_artifact, 8 | default_repo_access, 9 | ): 10 | resp = client.get( 11 | "/api/repos/{}/builds/{}/jobs/{}/artifacts/{}/download".format( 12 | default_repo.get_full_name(), 13 | default_build.number, 14 | default_job.number, 15 | default_artifact.id, 16 | ) 17 | ) 18 | assert resp.status_code == 302 19 | assert resp.headers["Location"] == "https://example.com/artifacts/junit.xml" 20 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_build_artifacts.py: -------------------------------------------------------------------------------- 1 | def test_build_artifacts_list( 2 | client, 3 | default_login, 4 | default_repo, 5 | default_build, 6 | default_job, 7 | default_artifact, 8 | default_repo_access, 9 | ): 10 | resp = client.get( 11 | "/api/repos/{}/builds/{}/artifacts".format( 12 | default_repo.get_full_name(), default_build.number 13 | ) 14 | ) 15 | assert resp.status_code == 200 16 | data = resp.json() 17 | assert len(data) == 1 18 | assert data[0]["id"] == str(default_artifact.id) 19 | 20 | 21 | def test_build_artifacts_list_empty( 22 | client, default_login, default_repo, default_build, default_job, default_repo_access 23 | ): 24 | resp = client.get( 25 | "/api/repos/{}/builds/{}/artifacts".format( 26 | default_repo.get_full_name(), default_build.number 27 | ) 28 | ) 29 | assert resp.status_code == 200 30 | data = resp.json() 31 | assert len(data) == 0 32 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_build_bundlestats.py: -------------------------------------------------------------------------------- 1 | from zeus import factories 2 | 3 | 4 | def test_build_bundle_stats( 5 | client, db_session, default_login, default_repo, default_build, default_repo_access 6 | ): 7 | job1 = factories.JobFactory.create(build=default_build) 8 | job2 = factories.JobFactory.create(build=default_build) 9 | 10 | bundle1 = factories.BundleFactory.create(job=job1, name="bar") 11 | factories.BundleAssetFactory.create(job=job1, bundle=bundle1, name="foo", size=50) 12 | 13 | factories.BundleFactory.create(job=job2, name="foo") 14 | db_session.flush() 15 | 16 | resp = client.get( 17 | "/api/repos/{}/builds/{}/bundle-stats".format( 18 | default_repo.get_full_name(), default_build.number 19 | ) 20 | ) 21 | assert resp.status_code == 200 22 | data = resp.json() 23 | assert len(data) == 2 24 | assert data[0]["name"] == "bar" 25 | assert data[0]["assets"] == [{"name": "foo", "size": 50}] 26 | assert data[1]["name"] == "foo" 27 | assert data[1]["assets"] == [] 28 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_build_details.py: -------------------------------------------------------------------------------- 1 | from zeus.models import ItemStat 2 | 3 | 4 | def test_build_details( 5 | client, db_session, default_login, default_repo, default_build, default_repo_access 6 | ): 7 | db_session.add(ItemStat(item_id=default_build.id, name="tests.count", value="1")) 8 | 9 | resp = client.get( 10 | "/api/repos/{}/builds/{}".format( 11 | default_repo.get_full_name(), default_build.number 12 | ) 13 | ) 14 | assert resp.status_code == 200 15 | data = resp.json() 16 | assert data["id"] == str(default_build.id) 17 | assert data["stats"]["tests"]["count"] == 1 18 | assert data["authors"] 19 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_github_organizations.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | ORG_LIST_RESPONSE = """[{ 4 | "id": 1, 5 | "login": "getsentry" 6 | }]""" 7 | 8 | 9 | def test_list_github_organizations( 10 | client, default_login, default_user, default_identity 11 | ): 12 | responses.add( 13 | "GET", 14 | "https://api.github.com/user/orgs", 15 | match_querystring=True, 16 | body=ORG_LIST_RESPONSE, 17 | ) 18 | 19 | resp = client.get("/api/github/orgs") 20 | assert resp.status_code == 200 21 | data = resp.json() 22 | assert len(data) == 1 23 | assert data[0]["name"] == "getsentry" 24 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_install_index.py: -------------------------------------------------------------------------------- 1 | def test_installation_details(client, default_user, default_build): 2 | resp = client.get("/api/install") 3 | assert resp.status_code == 200 4 | data = resp.json() 5 | assert data["stats"]["builds"] == { 6 | "created": {"24h": 1, "30d": 1}, 7 | "errored": {"24h": 0, "30d": 0}, 8 | } 9 | assert data["stats"]["jobs"] == { 10 | "created": {"24h": 0, "30d": 0}, 11 | "errored": {"24h": 0, "30d": 0}, 12 | } 13 | assert data["stats"]["repos"] == {"active": {"24h": 1, "30d": 1}} 14 | assert data["stats"]["users"] == {"active": {"24h": 1, "30d": 1}} 15 | assert data["config"]["debug"] is False 16 | assert data["config"]["environment"] 17 | assert data["config"]["release"] 18 | assert data["config"]["pubsubEndpoint"] == "http://localhost:8090" 19 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_repository_branches.py: -------------------------------------------------------------------------------- 1 | from zeus import factories 2 | from zeus.models import RepositoryAccess, RepositoryBackend, RepositoryProvider 3 | 4 | 5 | def test_repo_branch_list( 6 | client, db_session, default_login, default_user, git_repo_config, mock_vcs_server 7 | ): 8 | repo = factories.RepositoryFactory.create( 9 | backend=RepositoryBackend.git, 10 | provider=RepositoryProvider.github, 11 | url=git_repo_config.url, 12 | ) 13 | db_session.add(RepositoryAccess(user=default_user, repository=repo)) 14 | db_session.flush() 15 | 16 | resp = client.get("/api/repos/{}/branches".format(repo.get_full_name())) 17 | 18 | assert resp.status_code == 200 19 | data = resp.json() 20 | assert len(data) == 1 21 | assert data[0] == {"name": "master"} 22 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_repository_change_requests.py: -------------------------------------------------------------------------------- 1 | from zeus.models import ChangeRequest 2 | 3 | 4 | def test_create_change_request( 5 | client, default_login, default_repo, default_repo_access, default_revision 6 | ): 7 | resp = client.post( 8 | "/api/repos/{}/change-requests".format(default_repo.get_full_name()), 9 | json={ 10 | "message": "Hello world!", 11 | "provider": "github", 12 | "external_id": "123", 13 | "parent_ref": default_revision.sha, 14 | }, 15 | ) 16 | assert resp.status_code == 201, resp.json() 17 | data = resp.json() 18 | assert data["id"] 19 | 20 | cr = ChangeRequest.query.unrestricted_unsafe().get(data["id"]) 21 | assert cr.message == "Hello world!" 22 | assert cr.external_id == "123" 23 | assert cr.provider == "github" 24 | assert cr.parent_revision_sha == default_revision.sha 25 | assert cr.parent_ref == default_revision.sha 26 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_repository_index.py: -------------------------------------------------------------------------------- 1 | from zeus import factories 2 | 3 | 4 | def test_repo_list(client, default_login, default_repo, default_repo_access): 5 | resp = client.get("/api/repos") 6 | assert resp.status_code == 200 7 | data = resp.json() 8 | assert len(data) == 1 9 | assert data[0]["id"] == str(default_repo.id) 10 | assert data[0]["permissions"]["admin"] 11 | assert data[0]["permissions"]["read"] 12 | assert data[0]["permissions"]["write"] 13 | 14 | 15 | def test_repo_list_without_access(client, default_login, default_repo): 16 | resp = client.get("/api/repos") 17 | assert resp.status_code == 200 18 | data = resp.json() 19 | assert len(data) == 0 20 | 21 | 22 | def test_repo_list_excludes_public(client, default_login): 23 | factories.RepositoryFactory(name="public", public=True) 24 | resp = client.get("/api/repos") 25 | assert resp.status_code == 200 26 | data = resp.json() 27 | assert len(data) == 0 28 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_repository_revisions.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from zeus import factories 4 | from zeus.utils import timezone 5 | 6 | 7 | def test_repo_revision_list( 8 | client, 9 | db_session, 10 | default_login, 11 | default_revision, 12 | default_repo, 13 | default_repo_access, 14 | default_user, 15 | mock_vcs_server, 16 | ): 17 | 18 | mock_vcs_server.replace( 19 | mock_vcs_server.GET, 20 | "http://localhost:8070/stmt/log", 21 | json={"log": [{"sha": default_revision.sha}]}, 22 | ) 23 | 24 | factories.BuildFactory.create( 25 | revision=default_revision, date_created=timezone.now() - timedelta(minutes=1) 26 | ) 27 | factories.BuildFactory.create(revision=default_revision, passed=True) 28 | 29 | resp = client.get("/api/repos/{}/revisions".format(default_repo.get_full_name())) 30 | 31 | assert resp.status_code == 200 32 | data = resp.json() 33 | assert len(data) == 1 34 | assert data[0]["sha"] == default_revision.sha 35 | assert data[0]["latest_build"]["status"] == "finished" 36 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_repository_test_details.py: -------------------------------------------------------------------------------- 1 | from zeus.models import TestCaseMeta 2 | 3 | 4 | def test_repository_test_details( 5 | client, 6 | db_session, 7 | default_login, 8 | default_testcase, 9 | default_build, 10 | default_repo, 11 | default_repo_access, 12 | ): 13 | db_session.add( 14 | TestCaseMeta( 15 | repository_id=default_testcase.repository_id, 16 | name=default_testcase.name, 17 | hash=default_testcase.hash, 18 | first_build_id=default_build.id, 19 | ) 20 | ) 21 | 22 | resp = client.get( 23 | "/api/repos/{}/tests/{}".format( 24 | default_repo.get_full_name(), default_testcase.hash 25 | ) 26 | ) 27 | assert resp.status_code == 200 28 | data = resp.json() 29 | assert data["hash"] == str(default_testcase.hash) 30 | assert data["name"] == default_testcase.name 31 | assert data["first_build"]["id"] == str(default_build.id) 32 | assert data["last_build"]["id"] == str(default_build.id) 33 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_repository_test_history.py: -------------------------------------------------------------------------------- 1 | def test_repository_test_history( 2 | client, 3 | default_login, 4 | default_build, 5 | default_testcase, 6 | default_repo, 7 | default_repo_access, 8 | sqla_assertions, 9 | ): 10 | # Queries: 11 | # - Savepoint 12 | # - RepositoryAccess 13 | # - Repository 14 | # - RepositoryAccess? 15 | # - Aggregate test rows 16 | # - Build 17 | # - Build.authors 18 | # - Aggregate test rows count (paginator) 19 | with sqla_assertions.assert_statement_count(8): 20 | resp = client.get( 21 | "/api/repos/{}/tests/{}/history".format( 22 | default_repo.get_full_name(), default_testcase.hash 23 | ) 24 | ) 25 | 26 | assert resp.status_code == 200 27 | data = resp.json() 28 | assert len(data) == 1 29 | assert data[0]["hash"] == str(default_testcase.hash) 30 | assert data[0]["build"]["id"] == str(default_build.id) 31 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_test_details.py: -------------------------------------------------------------------------------- 1 | def test_test_details( 2 | client, default_login, default_testcase, default_repo, default_repo_access 3 | ): 4 | resp = client.get("/api/tests/{}".format(str(default_testcase.id))) 5 | assert resp.status_code == 200 6 | data = resp.json() 7 | assert data["id"] == str(default_testcase.id) 8 | assert data["hash"] == str(default_testcase.hash) 9 | assert data["name"] == default_testcase.name 10 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_user_details.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.models import ItemOption 3 | 4 | 5 | def test_user_details(client, default_login): 6 | resp = client.get("/api/users/me") 7 | assert resp.status_code == 200 8 | data = resp.json() 9 | assert data["email"] == default_login.email 10 | assert data["options"]["mail"]["notify_author"] == "1" 11 | 12 | 13 | def test_update_mail_notify_author(client, default_login, default_user): 14 | resp = client.put( 15 | "/api/users/me", json={"options": {"mail": {"notify_author": "0"}}} 16 | ) 17 | assert resp.status_code == 200 18 | data = resp.json() 19 | assert data["options"]["mail"]["notify_author"] == "0" 20 | 21 | assert db.session.query( 22 | ItemOption.query.filter( 23 | ItemOption.name == "mail.notify_author", 24 | ItemOption.item_id == default_user.id, 25 | ItemOption.value == "0", 26 | ).exists() 27 | ).scalar() 28 | -------------------------------------------------------------------------------- /tests/zeus/api/resources/test_user_emails.py: -------------------------------------------------------------------------------- 1 | def test_user_email_list(client, default_login): 2 | resp = client.get("/api/users/me/emails") 3 | assert resp.status_code == 200 4 | data = resp.json() 5 | assert len(data) == 1 6 | assert data[0]["email"] == default_login.email 7 | -------------------------------------------------------------------------------- /tests/zeus/api/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/api/schemas/__init__.py -------------------------------------------------------------------------------- /tests/zeus/artifacts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/artifacts/__init__.py -------------------------------------------------------------------------------- /tests/zeus/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/models/__init__.py -------------------------------------------------------------------------------- /tests/zeus/models/test_artifact.py: -------------------------------------------------------------------------------- 1 | from zeus.models import Artifact 2 | 3 | 4 | def test_file_path(default_repo, default_job, default_testcase): 5 | artifact = Artifact( 6 | repository=default_repo, job=default_job, testcase=default_testcase 7 | ) 8 | assert artifact.file.path == "artifacts" 9 | -------------------------------------------------------------------------------- /tests/zeus/models/test_build.py: -------------------------------------------------------------------------------- 1 | from zeus import auth, factories 2 | from zeus.constants import Permission 3 | from zeus.models import Build 4 | 5 | 6 | def test_tenant_limits_to_access(default_repo): 7 | auth.set_current_tenant(auth.Tenant(access={default_repo.id: Permission.read})) 8 | build = factories.BuildFactory(repository=default_repo) 9 | assert list(Build.query.all()) == [build] 10 | 11 | 12 | def test_tenant_allows_public_repos(default_repo): 13 | auth.set_current_tenant(auth.Tenant(access={default_repo.id: Permission.read})) 14 | repo = factories.RepositoryFactory(name="public", public=True) 15 | build = factories.BuildFactory(repository=repo) 16 | assert list(Build.query.all()) == [build] 17 | 18 | 19 | def test_tenant_allows_public_repos_with_acess(default_repo): 20 | auth.set_current_tenant(auth.Tenant()) 21 | repo = factories.RepositoryFactory(name="public", public=True) 22 | build = factories.BuildFactory(repository=repo) 23 | assert list(Build.query.all()) == [build] 24 | -------------------------------------------------------------------------------- /tests/zeus/models/test_hook.py: -------------------------------------------------------------------------------- 1 | from zeus import factories 2 | from zeus.models import Hook 3 | 4 | 5 | def test_get_required_hook_ids(default_repo): 6 | hook = factories.HookFactory.create(repository=default_repo, is_required=True) 7 | factories.HookFactory.create(repository=default_repo, is_required=False) 8 | 9 | assert Hook.get_required_hook_ids(default_repo.id) == [str(hook.id)] 10 | -------------------------------------------------------------------------------- /tests/zeus/models/test_repository.py: -------------------------------------------------------------------------------- 1 | from zeus import auth, factories 2 | from zeus.constants import Permission 3 | from zeus.models import Repository 4 | 5 | 6 | def test_tenant_does_not_query_repo_without_access(default_repo): 7 | auth.set_current_tenant(auth.Tenant()) 8 | assert list(Repository.query.all()) == [] 9 | 10 | 11 | def test_tenant_queries_repo_with_tenant(default_repo): 12 | auth.set_current_tenant(auth.Tenant(access={default_repo.id: Permission.read})) 13 | 14 | assert list(Repository.query.all()) == [default_repo] 15 | 16 | 17 | def test_tenant_allows_public_repos(default_repo): 18 | auth.set_current_tenant(auth.Tenant()) 19 | repo = factories.RepositoryFactory(name="public", public=True) 20 | assert list(Repository.query.all()) == [repo] 21 | 22 | 23 | def test_tenant_allows_public_repos_with_access(default_repo): 24 | auth.set_current_tenant(auth.Tenant(access={default_repo.id: Permission.read})) 25 | repo = factories.RepositoryFactory(name="public", public=True) 26 | assert list(Repository.query.all()) == [default_repo, repo] 27 | -------------------------------------------------------------------------------- /tests/zeus/notifications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/notifications/__init__.py -------------------------------------------------------------------------------- /tests/zeus/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/providers/__init__.py -------------------------------------------------------------------------------- /tests/zeus/providers/test_custom.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/providers/test_custom.py -------------------------------------------------------------------------------- /tests/zeus/providers/travis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/providers/travis/__init__.py -------------------------------------------------------------------------------- /tests/zeus/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/tasks/__init__.py -------------------------------------------------------------------------------- /tests/zeus/tasks/test_cleanup_pending_artifacts.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from zeus import factories 4 | from zeus.models import PendingArtifact 5 | from zeus.tasks import cleanup_pending_artifacts 6 | from zeus.utils import timezone 7 | 8 | 9 | def test_cleanup_pending_artifacts_current(mocker, db_session): 10 | pending_artifact = factories.PendingArtifactFactory.create() 11 | 12 | cleanup_pending_artifacts() 13 | 14 | assert PendingArtifact.query.unrestricted_unsafe().get(pending_artifact.id) 15 | 16 | 17 | def test_cleanup_pending_artifacts_old_file(mocker, db_session): 18 | pending_artifact = factories.PendingArtifactFactory.create( 19 | date_created=timezone.now() - timedelta(days=2) 20 | ) 21 | 22 | cleanup_pending_artifacts() 23 | 24 | assert not PendingArtifact.query.unrestricted_unsafe().get(pending_artifact.id) 25 | -------------------------------------------------------------------------------- /tests/zeus/tasks/test_deactivate_repo.py: -------------------------------------------------------------------------------- 1 | from zeus.constants import DeactivationReason 2 | from zeus.models import ItemOption, RepositoryStatus 3 | from zeus.tasks import deactivate_repo 4 | 5 | 6 | def test_deactivate_repo( 7 | db_session, default_repo, default_repo_access, default_user, outbox 8 | ): 9 | db_session.add( 10 | ItemOption(item_id=default_repo.id, name="auth.private-key", value="") 11 | ) 12 | db_session.flush() 13 | 14 | deactivate_repo(default_repo.id, DeactivationReason.invalid_pubkey) 15 | 16 | assert not ItemOption.query.filter( 17 | ItemOption.item_id == default_repo.id, ItemOption.name == "auth.private-key" 18 | ).first() 19 | assert default_repo.status == RepositoryStatus.inactive 20 | 21 | assert len(outbox) == 1 22 | msg = outbox[0] 23 | assert msg.subject == "Repository Disabled - getsentry/zeus" 24 | assert msg.recipients == [default_user.email] 25 | -------------------------------------------------------------------------------- /tests/zeus/tasks/test_delete_repo.py: -------------------------------------------------------------------------------- 1 | from zeus.tasks import delete_repo 2 | from zeus.models import Repository, RepositoryAccess, RepositoryStatus 3 | 4 | 5 | def test_delete_repo_existing_marked_inactive( 6 | db_session, default_repo, default_repo_access 7 | ): 8 | default_repo.status = RepositoryStatus.inactive 9 | db_session.add(default_repo) 10 | db_session.flush() 11 | 12 | delete_repo(default_repo.id) 13 | 14 | assert not Repository.query.unrestricted_unsafe().all() 15 | assert not RepositoryAccess.query.all() 16 | 17 | 18 | def test_delete_repo_existing_marked_active(default_repo, default_repo_access): 19 | assert default_repo.status == RepositoryStatus.active 20 | 21 | delete_repo(default_repo.id) 22 | 23 | assert Repository.query.unrestricted_unsafe().get(default_repo.id) 24 | -------------------------------------------------------------------------------- /tests/zeus/tasks/test_process_artifact.py: -------------------------------------------------------------------------------- 1 | from zeus import factories 2 | from zeus.constants import Status 3 | from zeus.tasks import process_artifact 4 | 5 | 6 | def test_aggregates_upon_completion(mocker, default_job): 7 | manager = mocker.Mock() 8 | 9 | artifact = factories.ArtifactFactory(job=default_job, queued=True) 10 | 11 | process_artifact(artifact_id=artifact.id, manager=manager) 12 | 13 | assert artifact.status == Status.finished 14 | 15 | manager.process.assert_called_once_with(artifact) 16 | -------------------------------------------------------------------------------- /tests/zeus/tasks/test_send_build_notifications.py: -------------------------------------------------------------------------------- 1 | from zeus import factories 2 | from zeus.tasks import send_build_notifications 3 | 4 | 5 | def test_sends_failure(mocker, db_session, default_revision, default_tenant): 6 | mock_send_email_notification = mocker.patch( 7 | "zeus.notifications.email.send_email_notification" 8 | ) 9 | 10 | build = factories.BuildFactory(revision=default_revision, failed=True) 11 | db_session.add(build) 12 | 13 | send_build_notifications(build.id) 14 | 15 | mock_send_email_notification.assert_called_once_with(build=build) 16 | 17 | 18 | def test_does_not_send_passing(mocker, db_session, default_revision, default_tenant): 19 | mock_send_email_notification = mocker.patch( 20 | "zeus.notifications.email.send_email_notification" 21 | ) 22 | 23 | build = factories.BuildFactory(revision=default_revision, passed=True) 24 | db_session.add(build) 25 | 26 | send_build_notifications(build.id) 27 | 28 | assert not mock_send_email_notification.mock_calls 29 | -------------------------------------------------------------------------------- /tests/zeus/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/utils/__init__.py -------------------------------------------------------------------------------- /tests/zeus/utils/test_trees.py: -------------------------------------------------------------------------------- 1 | from zeus.utils.trees import build_tree 2 | 3 | 4 | def test_build_tree(): 5 | test_names = [ 6 | "foo.bar.bar", 7 | "foo.bar.biz", 8 | "foo.biz", 9 | "blah.brah", 10 | "blah.blah.blah", 11 | ] 12 | 13 | result = build_tree(test_names, min_children=2) 14 | 15 | assert sorted(result) == ["blah", "foo"] 16 | 17 | result = build_tree(test_names, min_children=2, parent="foo") 18 | 19 | assert sorted(result) == ["foo.bar", "foo.biz"] 20 | 21 | result = build_tree(test_names, min_children=2, parent="foo.biz") 22 | 23 | assert result == set() 24 | -------------------------------------------------------------------------------- /tests/zeus/vcs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/vcs/__init__.py -------------------------------------------------------------------------------- /tests/zeus/vcs/test_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zeus.exceptions import UnknownRevision 4 | from zeus.vcs.client import vcs_client 5 | 6 | 7 | def test_log_with_unknown_revision(responses, default_repo): 8 | responses.add( 9 | responses.GET, 10 | f"http://localhost:8070/stmt/log?repo_id={default_repo.id}&parent=abcdef&offset=0&limit=100", 11 | json={"error": "invalid_ref", "ref": "abcdef"}, 12 | status=400, 13 | ) 14 | 15 | with pytest.raises(UnknownRevision) as exc: 16 | vcs_client.log(repo_id=default_repo.id, parent="abcdef") 17 | 18 | assert exc.ref == "abcdef" 19 | -------------------------------------------------------------------------------- /tests/zeus/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/web/__init__.py -------------------------------------------------------------------------------- /tests/zeus/web/hooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/tests/zeus/web/hooks/__init__.py -------------------------------------------------------------------------------- /tests/zeus/web/test_health_check.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def test_simple(client): 5 | resp = client.get("/healthz") 6 | assert resp.status_code == 200 7 | assert json.loads(resp.data) == {"ok": True} 8 | -------------------------------------------------------------------------------- /tests/zeus/web/test_index.py: -------------------------------------------------------------------------------- 1 | def test_index(client): 2 | resp = client.get("/") 3 | assert resp.status_code == 200 4 | -------------------------------------------------------------------------------- /webapp/actions/changeRequests.jsx: -------------------------------------------------------------------------------- 1 | import api from '../api'; 2 | import {LOAD_CHANGE_REQUEST_LIST, PRE_LOAD_CHANGE_REQUEST_LIST} from '../types'; 3 | 4 | export const fetchChangeRequests = query => { 5 | return dispatch => { 6 | dispatch({ 7 | type: PRE_LOAD_CHANGE_REQUEST_LIST 8 | }); 9 | api 10 | .get(`/change-requests`, { 11 | query 12 | }) 13 | .then(items => { 14 | dispatch({ 15 | type: LOAD_CHANGE_REQUEST_LIST, 16 | items 17 | }); 18 | }); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /webapp/actions/indicators.jsx: -------------------------------------------------------------------------------- 1 | import {ADD_INDICATOR, REMOVE_INDICATOR} from '../types'; 2 | 3 | let _lastId = 1; 4 | 5 | const _addIndicator = payload => { 6 | return { 7 | type: ADD_INDICATOR, 8 | payload 9 | }; 10 | }; 11 | 12 | const _removeIndicator = id => { 13 | return { 14 | type: REMOVE_INDICATOR, 15 | id 16 | }; 17 | }; 18 | 19 | export const addIndicator = (message, type, expiresAfter = 0) => { 20 | return dispatch => { 21 | let payload = { 22 | message, 23 | type, 24 | expiresAfter, 25 | id: _lastId++ 26 | }; 27 | dispatch(_addIndicator(payload)); 28 | 29 | if (expiresAfter > 0) { 30 | setTimeout(() => { 31 | dispatch(_removeIndicator(payload.id)); 32 | }, expiresAfter); 33 | } 34 | 35 | return payload; 36 | }; 37 | }; 38 | 39 | export const removeIndicator = indicator => { 40 | return dispatch => { 41 | dispatch(_removeIndicator(indicator.id)); 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /webapp/assets/IconCircleCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-icon-base'; 3 | 4 | function IconCircleCheck(props) { 5 | return ( 6 | 7 | 8 | 9 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default IconCircleCheck; 20 | -------------------------------------------------------------------------------- /webapp/assets/IconCircleCross.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-icon-base'; 3 | 4 | function IconCircleCross(props) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default IconCircleCross; 17 | -------------------------------------------------------------------------------- /webapp/assets/IconClock.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from 'react'; 3 | import Icon from 'react-icon-base'; 4 | 5 | export default props => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /webapp/assets/IconClock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /webapp/assets/screenshots/BuildDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/webapp/assets/screenshots/BuildDetails.png -------------------------------------------------------------------------------- /webapp/components/Badge.jsx: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/core'; 2 | import styled from '@emotion/styled'; 3 | 4 | export default styled.span` 5 | font-size: 65%; 6 | display: inline-block; 7 | padding: 0.25em 0.5em; 8 | margin-left: 5px; 9 | text-transform: uppercase; 10 | color: #111; 11 | font-weight: 700; 12 | border-radius: 8px; 13 | vertical-align: baseline; 14 | 15 | ${props => { 16 | switch (props.type) { 17 | case 'error': 18 | return css` 19 | background: #dc3545; 20 | `; 21 | case 'warning': 22 | return css` 23 | background: #ffc107; 24 | `; 25 | case 'dark': 26 | return css` 27 | color: #fff; 28 | background: #343a40; 29 | `; 30 | default: 31 | return css` 32 | color: #fff; 33 | background: #868e96; 34 | `; 35 | } 36 | }}; 37 | `; 38 | -------------------------------------------------------------------------------- /webapp/components/Breadcrumbs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Link} from 'react-router'; 4 | import styled from '@emotion/styled'; 5 | 6 | export const Breadcrumbs = styled.div` 7 | display: inline-block; 8 | `; 9 | 10 | export const CrumbLink = props => { 11 | return ( 12 | 13 | {props.children} 14 | 15 | ); 16 | }; 17 | 18 | CrumbLink.propTypes = { 19 | children: PropTypes.node, 20 | to: PropTypes.string.isRequired 21 | }; 22 | 23 | export const Crumb = styled.span` 24 | font-size: 22px; 25 | color: #111; 26 | 27 | a { 28 | color: inherit; 29 | } 30 | 31 | &:after { 32 | margin: 0 5px; 33 | content: ' / '; 34 | color: #111; 35 | } 36 | 37 | &:last-child { 38 | color: #111; 39 | &:after { 40 | display: none; 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /webapp/components/BuildLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import styled from '@emotion/styled'; 4 | 5 | import ObjectResult from './ObjectResult'; 6 | import Tooltip from './Tooltip'; 7 | 8 | export default styled(({repo, build, children, className, to}) => { 9 | return ( 10 | 13 |
{`#${build.number}: ${build.label}`}
14 | 15 | {`${build.result.toUpperCase()} after ${build.finished_at}`} 16 | 17 | }> 18 | 19 | {children} 20 | 21 |
22 | ); 23 | })` 24 | display: flex; 25 | justify-content: center; 26 | text-transform: none; 27 | width: 100%; 28 | color: #999; 29 | `; 30 | -------------------------------------------------------------------------------- /webapp/components/Container.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | `; 8 | -------------------------------------------------------------------------------- /webapp/components/Content.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | background: #fff; 5 | margin: 0 20px; 6 | `; 7 | -------------------------------------------------------------------------------- /webapp/components/DefinitionList.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.dl` 4 | margin: 0 0 20px; 5 | padding-left: ${props => props.prefixWidth || 140}px; 6 | 7 | &::after { 8 | content: ''; 9 | clear: both; 10 | display: table; 11 | } 12 | dt { 13 | float: left; 14 | clear: left; 15 | margin-left: -${props => props.prefixWidth || 140}px; 16 | width: ${props => (props.prefixWidth ? props.prefixWidth - 20 : 120)}px; 17 | white-space: nowrap; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | margin-bottom: 0.5rem; 21 | color: #999; 22 | } 23 | dd { 24 | margin-bottom: 0.5rem; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /webapp/components/Diff.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SyntaxHighlight from './SyntaxHighlight'; 4 | 5 | export default class Diff extends Component { 6 | static propTypes = { 7 | diff: PropTypes.string.isRequired 8 | }; 9 | 10 | render() { 11 | return {this.props.diff}; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/components/Duration.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {getDuration} from '../utils/duration'; 5 | 6 | export default class Duration extends Component { 7 | static propTypes = { 8 | ms: PropTypes.number.isRequired, 9 | short: PropTypes.bool 10 | }; 11 | 12 | static defaultProps = { 13 | short: true 14 | }; 15 | 16 | render() { 17 | let {ms, short} = this.props; 18 | return {getDuration(ms, short)}; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/components/Fieldset.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.fieldset` 4 | border: 0; 5 | margin-bottom: 20px; 6 | `; 7 | -------------------------------------------------------------------------------- /webapp/components/FileSize.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {getSize} from '../utils/fileSize'; 5 | 6 | export default class FileSize extends Component { 7 | static propTypes = { 8 | value: PropTypes.number.isRequired 9 | }; 10 | 11 | render() { 12 | return {getSize(this.props.value)}; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /webapp/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router'; 3 | import styled from '@emotion/styled'; 4 | 5 | class UnstyledFooter extends Component { 6 | render() { 7 | let props = this.props; 8 | let release = window.ZEUS_RELEASE ? window.ZEUS_RELEASE.substr(0, 14) : 'unknown'; 9 | return ( 10 |
11 |
12 | 15 | Zeus 16 | {' '} 17 | is Open Source Software 18 |
19 | 20 | Build {release} — Install Overview 21 | 22 |
23 | {props.children} 24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | export default styled(UnstyledFooter)` 31 | text-align: center; 32 | color: #333; 33 | font-size: 0.8em; 34 | padding: 20px 0; 35 | background: #fff; 36 | `; 37 | -------------------------------------------------------------------------------- /webapp/components/FormActions.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | border-top: 1px solid #e9ebec; 5 | background: none; 6 | padding: 20px 20px 0; 7 | margin-bottom: 20px; 8 | `; 9 | -------------------------------------------------------------------------------- /webapp/components/GitHubLoginButton.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Button from './Button'; 4 | 5 | export default class GitHubLoginButton extends Component { 6 | static propTypes = { 7 | url: PropTypes.string, 8 | next: PropTypes.string, 9 | text: PropTypes.string 10 | }; 11 | 12 | static defaultProps = { 13 | url: '/auth/github', 14 | text: 'Login with GitHub' 15 | }; 16 | 17 | render() { 18 | let {url, next, text} = this.props; 19 | let fullUrl = next 20 | ? url.indexOf('?') !== -1 21 | ? `${url}&next=${window.encodeURIComponent(next)}` 22 | : `${url}?next=${window.encodeURIComponent(next)}` 23 | : url; 24 | return ( 25 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webapp/components/HorizontalHeader.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Link} from 'react-router'; 4 | import styled from '@emotion/styled'; 5 | 6 | import Logo from '../assets/Logo'; 7 | 8 | export class HorizontalHeader extends Component { 9 | static propTypes = { 10 | children: PropTypes.node 11 | }; 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | {this.props.children} 20 | 21 | ); 22 | } 23 | } 24 | 25 | export const HeaderWrapper = styled.div` 26 | background: #403b5d; 27 | color: #fff; 28 | padding: 15px 30px; 29 | height: 60px; 30 | box-shadow: inset -5px 0 10px rgba(0, 0, 0, 0.1); 31 | `; 32 | 33 | export const HeaderLink = styled(Link)` 34 | color: #8783a3; 35 | font-weight: 400; 36 | `; 37 | 38 | export default HorizontalHeader; 39 | -------------------------------------------------------------------------------- /webapp/components/Icon.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div( 4 | { 5 | display: 'inline-block', 6 | verticalAlign: 'text-top', 7 | width: 16, 8 | height: 16, 9 | fontSize: 16 10 | }, 11 | props => ({ 12 | marginRight: props.mr ? 5 : 0 13 | }) 14 | ); 15 | -------------------------------------------------------------------------------- /webapp/components/IdentityNeedsUpgradeError.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import GitHubLoginButton from '../components/GitHubLoginButton'; 5 | import Modal from './Modal'; 6 | 7 | export default class IdentityNeedsUpgradeError extends Component { 8 | static propTypes = { 9 | location: PropTypes.object.isRequired, 10 | url: PropTypes.string.isRequired 11 | }; 12 | 13 | buildUrl() { 14 | let {location} = this.props; 15 | return `${location.pathname}${location.search || ''}`; 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 |

22 | You will need to grant additional permissions to Zeus to complete your request. 23 |

24 |

25 | 30 |

31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/components/Input.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.input` 4 | padding: 4px 8px; 5 | border: 1px solid #ddd; 6 | `; 7 | -------------------------------------------------------------------------------- /webapp/components/Label.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.label` 4 | font-size: 13px; 5 | font-weight: 500; 6 | text-transform: uppercase; 7 | margin: 0 0 20px; 8 | color: #767488; 9 | margin-bottom: 10px; 10 | display: block; 11 | `; 12 | -------------------------------------------------------------------------------- /webapp/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Content from './Content'; 5 | import Header from './Header'; 6 | import Footer from './Footer'; 7 | 8 | export default class Layout extends Component { 9 | static propTypes = { 10 | children: PropTypes.node, 11 | title: PropTypes.string 12 | }; 13 | 14 | render() { 15 | return ( 16 |
17 |
18 | {this.props.children} 19 |
20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webapp/components/ListItemLink.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from 'react-router'; 2 | import styled from '@emotion/styled'; 3 | 4 | const ListItemLink = styled(Link)` 5 | display: block; 6 | 7 | &:hover { 8 | background-color: #f0eff5; 9 | } 10 | 11 | &.${props => props.activeClassName} { 12 | color: #fff; 13 | background: #7b6be6; 14 | 15 | > div { 16 | color: #fff !important; 17 | 18 | svg { 19 | color: #fff; 20 | opacity: 0.5; 21 | } 22 | } 23 | } 24 | `; 25 | 26 | ListItemLink.defaultProps = { 27 | activeClassName: 'active' 28 | }; 29 | 30 | export default ListItemLink; 31 | -------------------------------------------------------------------------------- /webapp/components/Nav.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from 'react-router'; 2 | import styled from '@emotion/styled'; 3 | 4 | export const Nav = styled.div` 5 | display: inline-block; 6 | `; 7 | export default Nav; 8 | 9 | export const NavItem = styled(Link)` 10 | cursor: pointer; 11 | float: left; 12 | font-size: 15px; 13 | color: #333; 14 | margin-left: 10px; 15 | padding: 5px 10px; 16 | border: 3px solid #fff; 17 | border-radius: 4px; 18 | 19 | &:hover { 20 | color: #333; 21 | } 22 | 23 | &.active, 24 | .${props => props.activeClassName} { 25 | border-color: #7b6be6; 26 | color: #7b6be6; 27 | } 28 | `; 29 | 30 | NavItem.defaultProps = { 31 | activeClassName: 'active' 32 | }; 33 | -------------------------------------------------------------------------------- /webapp/components/NavHeading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | export default styled(({...props}) => { 5 | return
{props.children}
; 6 | })` 7 | font-size: 13px; 8 | font-weight: 500; 9 | text-transform: uppercase; 10 | margin: 0 0 30px; 11 | color: #767488; 12 | `; 13 | -------------------------------------------------------------------------------- /webapp/components/ObjectCoverage.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class ObjectCoverage extends Component { 5 | static propTypes = { 6 | data: PropTypes.object.isRequired, 7 | diff: PropTypes.bool 8 | }; 9 | 10 | static defaultProps = { 11 | diff: true 12 | }; 13 | 14 | getCoverage() { 15 | let {data, diff} = this.props; 16 | if (data.status !== 'finished') return ''; 17 | if (!data.stats.coverage) return ''; 18 | let covStats = data.stats.coverage; 19 | let linesCovered = diff ? covStats.diff_lines_covered : covStats.lines_covered, 20 | linesUncovered = diff ? covStats.diff_lines_uncovered : covStats.lines_uncovered; 21 | let totalLines = linesCovered + linesUncovered; 22 | if (totalLines === 0) return ''; 23 | if (linesCovered === 0) return '0%'; 24 | return `${parseInt((linesCovered / totalLines) * 100, 10)}%`; 25 | } 26 | 27 | render() { 28 | return {this.getCoverage()}; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webapp/components/PageLoading.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import InternalError from './InternalError'; 5 | 6 | export default class PageLoading extends Component { 7 | static propTypes = { 8 | isLoading: PropTypes.bool, 9 | error: PropTypes.object 10 | }; 11 | 12 | render() { 13 | let {isLoading, error} = this.props; 14 | if (isLoading) { 15 | return
Loading...
; 16 | } else if (error) { 17 | return ; 18 | } else { 19 | return null; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webapp/components/Panel.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | background: #fff; 5 | margin-bottom: 20px; 6 | `; 7 | -------------------------------------------------------------------------------- /webapp/components/RepositoryContent.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable react/display-name */ 3 | 4 | import React from 'react'; 5 | 6 | import Content from '../components/Content'; 7 | 8 | export default ({children}) => { 9 | return {children}; 10 | }; 11 | -------------------------------------------------------------------------------- /webapp/components/RepositoryHeader.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Header from '../components/Header'; 5 | import Nav, {NavItem} from '../components/Nav'; 6 | 7 | export default class RepositoryHeader extends Component { 8 | static contextTypes = { 9 | repo: PropTypes.object.isRequired 10 | }; 11 | 12 | render() { 13 | let {repo} = this.context; 14 | let basePath = `/${repo.full_name}`; 15 | 16 | return ( 17 |
18 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/components/RepositoryNav.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | overflow: hidden; 5 | margin: -20px 0 20px; 6 | padding: 0 20px; 7 | box-shadow: inset 0 -1px 0 #dbdae3; 8 | `; 9 | -------------------------------------------------------------------------------- /webapp/components/RepositoryNavItem.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from 'react-router'; 2 | import styled from '@emotion/styled'; 3 | 4 | const RepositoryNavItem = styled(Link)` 5 | cursor: pointer; 6 | float: left; 7 | font-size: 15px; 8 | color: #aaa7bb; 9 | padding: 10px 20px; 10 | border-left: 1px solid #dbdae3; 11 | 12 | &:last-item { 13 | border-right: 1px solid #dbdae3; 14 | } 15 | 16 | &.active { 17 | background: #7b6be6; 18 | color: #fff; 19 | } 20 | 21 | &.${props => props.activeClassName} { 22 | background: #7b6be6; 23 | color: #fff; 24 | } 25 | `; 26 | 27 | RepositoryNavItem.defaultProps = { 28 | activeClassName: 'active' 29 | }; 30 | 31 | export default RepositoryNavItem; 32 | -------------------------------------------------------------------------------- /webapp/components/ResultGridRow.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | display: block; 5 | padding: 8px 2px; 6 | color: #333; 7 | border-bottom: 1px solid #eee; 8 | font-size: 14px; 9 | `; 10 | -------------------------------------------------------------------------------- /webapp/components/ScrollView.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | position: absolute; 5 | top: 68px; /* TODO(ckj): calculate this dynamically */ 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | overflow: auto; 10 | padding: 20px 0; 11 | background: #fff; 12 | `; 13 | -------------------------------------------------------------------------------- /webapp/components/Section.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | margin-bottom: 20px; 5 | 6 | > ol > li { 7 | margin-bottom: 20px; 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /webapp/components/SectionHeading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | export default styled(({...props}) => { 5 | return
{props.children}
; 6 | })` 7 | font-size: 16px; 8 | line-height: 16px; 9 | font-weight: 500; 10 | text-transform: uppercase; 11 | margin: 0 0 20px; 12 | letter-spacing: -1px; 13 | color: #111; 14 | 15 | small { 16 | float: right; 17 | color: #999; 18 | font-size: 14px; 19 | line-height: 16px; 20 | font-weight: normal; 21 | text-transform: none; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /webapp/components/SectionSubheading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | export default styled(({...props}) => { 5 | return
{props.children}
; 6 | })` 7 | font-size: 12px; 8 | font-weight: 500; 9 | text-transform: uppercase; 10 | margin: 0 0 20px; 11 | color: #767488; 12 | `; 13 | -------------------------------------------------------------------------------- /webapp/components/Select.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.select` 4 | padding: 4px 8px; 5 | border: 1px solid #ddd; 6 | `; 7 | -------------------------------------------------------------------------------- /webapp/components/TabbedNav.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export default styled.div` 4 | margin: 0 0 20px; 5 | border-bottom: 4px solid #e0e4e8; 6 | `; 7 | -------------------------------------------------------------------------------- /webapp/components/TabbedNavItem.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from 'react-router'; 2 | import styled from '@emotion/styled'; 3 | 4 | const TabbedNavItem = styled(Link)` 5 | cursor: pointer; 6 | display: inline-block; 7 | font-size: 15px; 8 | color: #aaa7bb; 9 | margin-right: 20px; 10 | padding: 0 0 10px; 11 | margin-bottom: -4px; 12 | border-bottom: 4px solid transparent; 13 | text-decoration: none; 14 | 15 | &.active, 16 | ${props => props.activeClassName} { 17 | color: #39364e; 18 | border-bottom-color: #7b6be6; 19 | } 20 | `; 21 | 22 | TabbedNavItem.defaultProps = { 23 | activeClassName: 'active' 24 | }; 25 | 26 | export default TabbedNavItem; 27 | -------------------------------------------------------------------------------- /webapp/components/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import ReachTooltip from '@reach/tooltip'; 3 | 4 | export default styled(ReachTooltip)` 5 | font-size: 14px; 6 | font-family: 'Rubik', Helvetica, sans-serif; 7 | padding: 8px; 8 | background: #eee; 9 | color: #333; 10 | border: 2px solid #ddd; 11 | border-radius: 4px; 12 | 13 | h6 { 14 | font-size: 16px; 15 | margin-bottom: 5px; 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /webapp/components/Tree.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from 'react-router'; 2 | import styled from '@emotion/styled'; 3 | 4 | import Section from './Section'; 5 | 6 | export const Tree = styled.div` 7 | margin-bottom: 20px; 8 | `; 9 | 10 | export const Leaf = styled(Link)` 11 | color: #000; 12 | font-weight: 400; 13 | 14 | &:after { 15 | margin: 0 5px; 16 | content: ' / '; 17 | color: #ddd; 18 | } 19 | 20 | &:last-child { 21 | color: #666; 22 | &:after { 23 | display: none; 24 | } 25 | } 26 | `; 27 | 28 | export const TreeWrapper = styled(Section)` 29 | border: 2px solid #eee; 30 | padding: 10px; 31 | `; 32 | 33 | export const TreeSummary = styled.div` 34 | font-size: 0.8em; 35 | 36 | p { 37 | margin-bottom: 10px; 38 | } 39 | 40 | p:last-child { 41 | margin-bottom: 0; 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /webapp/components/__tests__/BuildList.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'enzyme'; 3 | 4 | import BuildList from '../BuildList'; 5 | 6 | describe('BuildList', () => { 7 | it('renders', () => { 8 | let repository = TestStubs.Repository(); 9 | const tree = render( 10 | 17 | ); 18 | expect(tree).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /webapp/components/__tests__/Collapsable.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'enzyme'; 3 | 4 | import Collapsable from '../Collapsable'; 5 | 6 | describe('Collapsable', () => { 7 | it('renders collapsed', () => { 8 | const tree = render( 9 | 10 |
1
11 |
2
12 |
3
13 |
4
14 |
15 | ); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /webapp/components/__tests__/CoverageSummary.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'enzyme'; 3 | 4 | import CoverageSummary from '../CoverageSummary'; 5 | 6 | const FIXTURE = [ 7 | { 8 | data: 'NNNNNNNNNNNNNNNNNNCCNNNCCCCCUCCCNNCNNNUNNNNNNNNNNC', 9 | diff_lines_covered: 2.0, 10 | diff_lines_uncovered: 0.0, 11 | filename: 'webapp/components/Collapsable.jsx', 12 | lines_covered: 12.0, 13 | lines_uncovered: 2.0 14 | } 15 | ]; 16 | 17 | describe('CoverageSummary', () => { 18 | it('renders', () => { 19 | let context = TestStubs.standardContext(); 20 | const tree = render( 21 | , 27 | context 28 | ); 29 | expect(tree).toMatchSnapshot(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /webapp/components/__tests__/FileSize.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | 4 | import FileSize from '../FileSize'; 5 | 6 | describe('FileSize', () => { 7 | it('renders bytes', () => { 8 | const tree = shallow(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /webapp/components/__tests__/ObjectAuthor.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | 4 | import ObjectAuthor from '../ObjectAuthor'; 5 | 6 | describe('ObjectAuthor', () => { 7 | it('renders author', () => { 8 | const tree = shallow( 9 | 19 | ); 20 | expect(tree).toMatchSnapshot(); 21 | }); 22 | 23 | it('renders anonymous', () => { 24 | const tree = shallow( 25 | 30 | ); 31 | expect(tree).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /webapp/components/__tests__/ObjectDuration.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'enzyme'; 3 | 4 | import ObjectDuration from '../ObjectDuration'; 5 | 6 | describe('ObjectDuration', () => { 7 | it('renders unfinished', () => { 8 | const tree = render( 9 | 15 | ); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | 19 | it('renders in-progress', () => { 20 | const tree = render( 21 | 27 | ); 28 | expect(tree).toMatchSnapshot(); 29 | }); 30 | 31 | it('renders finished', () => { 32 | const tree = render( 33 | 39 | ); 40 | expect(tree).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /webapp/components/__tests__/RevisionList.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {mount} from 'enzyme'; 3 | 4 | import RevisionList from '../RevisionList'; 5 | 6 | describe('RevisionList', () => { 7 | it('renders', done => { 8 | let repo = TestStubs.Repository(); 9 | let revision = TestStubs.Revision(); 10 | let location = TestStubs.location({pathname: `/${repo.provider}/${repo.full_name}`}); 11 | 12 | let context = TestStubs.standardContext(); 13 | 14 | const wrapper = mount( 15 | , 25 | context 26 | ); 27 | setTimeout(() => { 28 | expect(wrapper).toMatchSnapshot(); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /webapp/components/__tests__/TimeSince.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'enzyme'; 3 | 4 | import TimeSince from '../TimeSince'; 5 | 6 | describe('TimeSince', () => { 7 | it('renders default', () => { 8 | const tree = render(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | 12 | it('renders 24 hour clock', () => { 13 | const tree = render(); 14 | expect(tree).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /webapp/components/__tests__/__snapshots__/Collapsable.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Collapsable renders collapsed 1`] = ` 4 |
5 |
6 | 1 7 |
8 |
9 | 2 10 |
11 | 14 |
18 |
21 | Show 2 other item(s) 22 |
23 |
24 |
25 |
26 | `; 27 | -------------------------------------------------------------------------------- /webapp/components/__tests__/__snapshots__/FileSize.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FileSize renders bytes 1`] = ` 4 | 5 | 413 B 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /webapp/components/__tests__/__snapshots__/ObjectAuthor.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ObjectAuthor renders anonymous 1`] = `""`; 4 | 5 | exports[`ObjectAuthor renders author 1`] = ` 6 | 7 | 15 | 16 | 23 | 24 | Foo Bar 25 | 26 | 27 | `; 28 | -------------------------------------------------------------------------------- /webapp/components/__tests__/__snapshots__/ObjectDuration.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ObjectDuration renders finished 1`] = ` 4 | 5 | 2m 6 | 7 | `; 8 | 9 | exports[`ObjectDuration renders in-progress 1`] = ` 10 | 11 | 6s 12 | 13 | `; 14 | 15 | exports[`ObjectDuration renders unfinished 1`] = `null`; 16 | -------------------------------------------------------------------------------- /webapp/components/__tests__/__snapshots__/TimeSince.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TimeSince renders 24 hour clock 1`] = ` 4 | 10 | `; 11 | 12 | exports[`TimeSince renders default 1`] = ` 13 | 19 | `; 20 | -------------------------------------------------------------------------------- /webapp/errors.jsx: -------------------------------------------------------------------------------- 1 | class ApiError extends Error { 2 | constructor(msg, code) { 3 | super(msg); 4 | this.code = code; 5 | } 6 | } 7 | 8 | class ResourceNotFound extends Error { 9 | static code = 404; 10 | } 11 | 12 | class NetworkError extends Error { 13 | constructor(msg, code) { 14 | super(msg); 15 | this.code = code; 16 | } 17 | } 18 | 19 | export {ApiError, ResourceNotFound, NetworkError}; 20 | -------------------------------------------------------------------------------- /webapp/middleware/sentry.jsx: -------------------------------------------------------------------------------- 1 | import {SET_CURRENT_AUTH} from '../types'; 2 | 3 | import * as Sentry from '@sentry/browser'; 4 | 5 | const createMiddleware = Sentry => { 6 | return () => next => action => { 7 | if (action.type === SET_CURRENT_AUTH) { 8 | let {isAuthenticated, user} = action.payload; 9 | if (!isAuthenticated) { 10 | Sentry.setUser(null); 11 | } else { 12 | Sentry.setUser({ 13 | id: user.id, 14 | email: user.email 15 | }); 16 | } 17 | } 18 | return next(action); 19 | }; 20 | }; 21 | 22 | export default createMiddleware(Sentry); 23 | -------------------------------------------------------------------------------- /webapp/pages/BuildArtifacts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ArtifactsList from '../components/ArtifactsList'; 5 | import AsyncPage from '../components/AsyncPage'; 6 | import Section from '../components/Section'; 7 | 8 | export default class BuildArtifacts extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | build: PropTypes.object.isRequired, 12 | repo: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {buildNumber} = this.props.params; 18 | return [['artifacts', `/repos/${repo.full_name}/builds/${buildNumber}/artifacts`]]; 19 | } 20 | 21 | renderBody() { 22 | return ( 23 |
24 | {this.state.artifacts.length ? ( 25 | 26 | ) : ( 27 | This build did not produce artifacts. 28 | )} 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webapp/pages/BuildCoverage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import BuildCoverageComponent from '../components/BuildCoverage'; 6 | 7 | export default class BuildCoverageTree extends AsyncPage { 8 | static contextTypes = { 9 | ...AsyncPage.contextTypes, 10 | build: PropTypes.object.isRequired, 11 | repo: PropTypes.object.isRequired 12 | }; 13 | 14 | getEndpoints() { 15 | let {repo} = this.context; 16 | let {buildNumber} = this.props.params; 17 | return [ 18 | [ 19 | 'result', 20 | `/repos/${repo.full_name}/builds/${buildNumber}/file-coverage-tree`, 21 | {query: this.props.location.query} 22 | ] 23 | ]; 24 | } 25 | 26 | renderBody() { 27 | return ; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/pages/BuildDetails.jsx: -------------------------------------------------------------------------------- 1 | import BuildDetailsBase from '../components/BuildDetailsBase'; 2 | 3 | export default class BuildDetails extends BuildDetailsBase { 4 | getBuildEndpoint() { 5 | let {repo} = this.context; 6 | let {buildNumber} = this.props.params; 7 | return `/repos/${repo.full_name}/builds/${buildNumber}`; 8 | } 9 | 10 | getBaseRoute() { 11 | let {repo} = this.context; 12 | let {buildNumber} = this.props.params; 13 | return `/${repo.full_name}/builds/${buildNumber}`; 14 | } 15 | 16 | getTitle() { 17 | let {repo} = this.context; 18 | let {buildNumber} = this.props.params; 19 | return `${repo.owner_name}/${repo.name}#${buildNumber}`; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/pages/BuildDiff.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import Diff from '../components/Diff'; 6 | import Section from '../components/Section'; 7 | 8 | export default class BuildDiff extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | repo: PropTypes.object.isRequired, 12 | build: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {buildNumber} = this.props.params; 18 | return [['diff', `/repos/${repo.full_name}/builds/${buildNumber}/diff`]]; 19 | } 20 | 21 | renderBody() { 22 | return ( 23 |
24 | {this.state.diff.diff ? ( 25 | 26 | ) : ( 27 |
28 | No diff information was available for this build. 29 |
30 | )} 31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/pages/BuildOverview.jsx: -------------------------------------------------------------------------------- 1 | import BuildOverviewBase from '../components/BuildOverviewBase'; 2 | 3 | export default class BuildOverview extends BuildOverviewBase { 4 | getBuildEndpoint() { 5 | let {repo} = this.context; 6 | let {buildNumber} = this.props.params; 7 | return `/repos/${repo.full_name}/builds/${buildNumber}`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webapp/pages/BuildStyleViolationList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import Section from '../components/Section'; 6 | import StyleViolationList from '../components/StyleViolationList'; 7 | 8 | export default class BuildStyleViolationList extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | build: PropTypes.object.isRequired, 12 | repo: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {buildNumber} = this.props.params; 18 | return [ 19 | ['violationList', `/repos/${repo.full_name}/builds/${buildNumber}/style-violations`] 20 | ]; 21 | } 22 | 23 | renderBody() { 24 | return ( 25 |
26 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/pages/DashboardOrWelcome.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | 5 | import Dashboard from './Dashboard'; 6 | import Welcome from './Welcome'; 7 | 8 | class DashboardOrWelcome extends Component { 9 | static propTypes = { 10 | isAuthenticated: PropTypes.bool, 11 | user: PropTypes.object 12 | }; 13 | 14 | render() { 15 | if (this.props.isAuthenticated) return ; 16 | return ; 17 | } 18 | } 19 | 20 | export default connect(({auth}) => ({ 21 | user: auth.user, 22 | isAuthenticated: auth.isAuthenticated 23 | }))(DashboardOrWelcome); 24 | -------------------------------------------------------------------------------- /webapp/pages/RepositoryFileCoverage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import BuildCoverageComponent from '../components/BuildCoverage'; 6 | 7 | export default class RepositoryFileCoverage extends AsyncPage { 8 | static contextTypes = { 9 | ...AsyncPage.contextTypes, 10 | repo: PropTypes.object.isRequired 11 | }; 12 | 13 | getEndpoints() { 14 | let {repo} = this.context; 15 | return [ 16 | [ 17 | 'result', 18 | `/repos/${repo.full_name}/file-coverage-tree`, 19 | {query: this.props.location.query} 20 | ] 21 | ]; 22 | } 23 | 24 | renderBody() { 25 | return ; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webapp/pages/RepositoryReportsLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Flex, Box} from '@rebass/grid/emotion'; 4 | 5 | import AsyncPage from '../components/AsyncPage'; 6 | import ListLink from '../components/ListLink'; 7 | 8 | export default class RepositoryReportsLayout extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | repo: PropTypes.object.isRequired 12 | }; 13 | 14 | renderBody() { 15 | let {repo} = this.context; 16 | return ( 17 | 18 | 19 |
    20 | Overview 21 | Code Coverage 22 | Tests 23 |
24 |
25 | {this.props.children} 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/pages/RepositorySettingsLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Flex, Box} from '@rebass/grid/emotion'; 4 | 5 | import AsyncPage from '../components/AsyncPage'; 6 | import ListLink from '../components/ListLink'; 7 | 8 | export default class RepositorySettingsLayout extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | repo: PropTypes.object.isRequired 12 | }; 13 | 14 | renderBody() { 15 | let {repo} = this.context; 16 | return ( 17 | 18 | 19 |
    20 | General 21 | Hooks 22 |
23 |
24 | {this.props.children} 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webapp/pages/RepositoryTests.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import TabbedNav from '../components/TabbedNav'; 6 | import TabbedNavItem from '../components/TabbedNavItem'; 7 | 8 | export default class RepositoryTests extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | repo: PropTypes.object.isRequired 12 | }; 13 | 14 | renderBody() { 15 | let {repo} = this.context; 16 | let basePath = `/${repo.full_name}/reports/tests`; 17 | return ( 18 |
19 | 20 | All Tests 21 | 22 | Tree View 23 | 24 | 25 | {this.props.children} 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webapp/pages/RevisionArtifacts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ArtifactsList from '../components/ArtifactsList'; 5 | import AsyncPage from '../components/AsyncPage'; 6 | import Section from '../components/Section'; 7 | 8 | export default class RevisionArtifacts extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | build: PropTypes.object.isRequired, 12 | repo: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {sha} = this.props.params; 18 | return [['artifacts', `/repos/${repo.full_name}/revisions/${sha}/artifacts`]]; 19 | } 20 | 21 | renderBody() { 22 | return ( 23 |
24 | {this.state.artifacts.length ? ( 25 | 26 | ) : ( 27 | This build did not produce artifacts. 28 | )} 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webapp/pages/RevisionCoverage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import BuildCoverage from '../components/BuildCoverage'; 6 | import Section from '../components/Section'; 7 | 8 | export default class RevisionCoverage extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | repo: PropTypes.object.isRequired 12 | }; 13 | 14 | getEndpoints() { 15 | let {repo} = this.context; 16 | let {sha} = this.props.params; 17 | return [ 18 | [ 19 | 'result', 20 | `/repos/${repo.full_name}/revisions/${sha}/file-coverage-tree`, 21 | {query: this.props.location.query} 22 | ] 23 | ]; 24 | } 25 | 26 | renderBody() { 27 | return ( 28 |
29 | 34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webapp/pages/RevisionDetails.jsx: -------------------------------------------------------------------------------- 1 | import BuildDetailsBase from '../components/BuildDetailsBase'; 2 | 3 | export default class RevisionDetails extends BuildDetailsBase { 4 | getBuildEndpoint() { 5 | let {repo} = this.context; 6 | let {sha} = this.props.params; 7 | return `/repos/${repo.full_name}/revisions/${sha}`; 8 | } 9 | 10 | getBaseRoute() { 11 | let {repo} = this.context; 12 | let {sha} = this.props.params; 13 | return `/${repo.full_name}/revisions/${sha}`; 14 | } 15 | 16 | getTitle() { 17 | return 'Build Details'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /webapp/pages/RevisionDiff.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import Diff from '../components/Diff'; 6 | import Section from '../components/Section'; 7 | 8 | export default class RevisionDiff extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | repo: PropTypes.object.isRequired, 12 | build: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {sha} = this.props.params; 18 | return [['diff', `/repos/${repo.full_name}/revisions/${sha}/diff`]]; 19 | } 20 | 21 | renderBody() { 22 | return ( 23 |
24 | {this.state.diff.diff ? ( 25 | 26 | ) : ( 27 |
28 | No diff information was available for this build. 29 |
30 | )} 31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webapp/pages/RevisionOverview.jsx: -------------------------------------------------------------------------------- 1 | import BuildOverviewBase from '../components/BuildOverviewBase'; 2 | 3 | export default class RevisionOverview extends BuildOverviewBase { 4 | getBuildEndpoint() { 5 | let {repo} = this.context; 6 | let {sha} = this.props.params; 7 | return `/repos/${repo.full_name}/revisions/${sha}`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webapp/pages/RevisionStyleViolationList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import Section from '../components/Section'; 6 | import StyleViolationList from '../components/StyleViolationList'; 7 | 8 | export default class RevisionStyleViolationList extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | build: PropTypes.object.isRequired, 12 | repo: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {sha} = this.props.params; 18 | return [ 19 | ['violationList', `/repos/${repo.full_name}/revisions/${sha}/style-violations`] 20 | ]; 21 | } 22 | 23 | renderBody() { 24 | return ( 25 |
26 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/pages/RevisionTestList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AsyncPage from '../components/AsyncPage'; 5 | import Section from '../components/Section'; 6 | import TestList from '../components/TestList'; 7 | 8 | export default class RevisionTestList extends AsyncPage { 9 | static contextTypes = { 10 | ...AsyncPage.contextTypes, 11 | build: PropTypes.object.isRequired, 12 | repo: PropTypes.object.isRequired 13 | }; 14 | 15 | getEndpoints() { 16 | let {repo} = this.context; 17 | let {sha} = this.props.params; 18 | return [['testList', `/repos/${repo.full_name}/revisions/${sha}/tests`]]; 19 | } 20 | 21 | renderBody() { 22 | return ( 23 |
24 | 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webapp/pages/__tests__/App.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | import sinon from 'sinon'; 4 | 5 | import {App} from '../App'; 6 | 7 | describe('App', () => { 8 | it('renders without crashing', () => { 9 | let authSession = sinon.spy(); 10 | const tree = shallow(); 11 | expect(tree).toMatchSnapshot(); 12 | expect(authSession.called).toBe(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /webapp/pages/__tests__/RepositoryHookCreate.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'enzyme'; 3 | import sinon from 'sinon'; 4 | 5 | import {RepositoryHookCreate} from '../RepositoryHookCreate'; 6 | 7 | describe('RepositoryHookCreate', () => { 8 | it('renders', () => { 9 | const tree = render( 10 | , 11 | { 12 | context: {repo: TestStubs.Repository(), router: TestStubs.router()} 13 | } 14 | ); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /webapp/pages/__tests__/__snapshots__/App.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App renders without crashing 1`] = ` 4 |
5 | 6 | 7 | 8 | 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /webapp/pages/__tests__/__snapshots__/RepositoryHookCreate.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RepositoryHookCreate renders 1`] = ` 4 |
5 |
6 |
9 | Create Hook 10 |
11 |
12 |

13 | Hooks lets you easily upsert build information into Zeus. They're a set of credentials that are bound to this repository. Once you've created the hook, it can be used to submit build and job information, as well as to upload artifacts (for example, via 14 | 17 | zeus-cli 18 | 19 | ). 20 |

21 |

22 | To create a new hook, select a provider: 23 |

24 |

25 | 28 | Travis CI 29 | 30 | 31 | 34 | Custom 35 | 36 |

37 |
38 | `; 39 | -------------------------------------------------------------------------------- /webapp/reducers/auth.jsx: -------------------------------------------------------------------------------- 1 | import {SET_CURRENT_AUTH} from '../types'; 2 | 3 | const initialState = { 4 | // default to unknown state 5 | isAuthenticated: null, 6 | 7 | emails: null, 8 | user: null, 9 | identities: null 10 | }; 11 | 12 | export default (state = initialState, action = {}) => { 13 | switch (action.type) { 14 | case SET_CURRENT_AUTH: 15 | return {...action.payload}; 16 | default: 17 | return state; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /webapp/reducers/changeRequests.jsx: -------------------------------------------------------------------------------- 1 | import {LOAD_CHANGE_REQUEST_LIST, PRE_LOAD_CHANGE_REQUEST_LIST} from '../types'; 2 | 3 | const initialState = { 4 | items: [], 5 | links: {}, 6 | loaded: false 7 | }; 8 | 9 | export default (state = initialState, action = {}) => { 10 | switch (action.type) { 11 | case PRE_LOAD_CHANGE_REQUEST_LIST: 12 | return { 13 | ...state, 14 | loaded: false 15 | }; 16 | case LOAD_CHANGE_REQUEST_LIST: 17 | return { 18 | ...state, 19 | items: [...action.items], 20 | links: {...action.items.links}, 21 | loaded: true 22 | }; 23 | default: 24 | return state; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /webapp/reducers/index.jsx: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | 3 | import auth from './auth'; 4 | import builds from './builds'; 5 | import changeRequests from './changeRequests'; 6 | import indicators from './indicators'; 7 | import repos from './repos'; 8 | import revisions from './revisions'; 9 | import stream from './stream'; 10 | 11 | export default combineReducers({ 12 | auth, 13 | builds, 14 | changeRequests, 15 | indicators, 16 | repos, 17 | revisions, 18 | stream 19 | }); 20 | -------------------------------------------------------------------------------- /webapp/reducers/indicators.jsx: -------------------------------------------------------------------------------- 1 | import {ADD_INDICATOR, REMOVE_INDICATOR} from '../types'; 2 | 3 | const initialState = { 4 | items: [] 5 | }; 6 | 7 | export default (state = initialState, action = {}) => { 8 | switch (action.type) { 9 | case ADD_INDICATOR: 10 | return { 11 | ...state, 12 | items: [...state.items, action.payload] 13 | }; 14 | case REMOVE_INDICATOR: 15 | return { 16 | ...state, 17 | items: [...state.items.filter(m => m.id !== action.id)] 18 | }; 19 | default: 20 | return state; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /webapp/reducers/stream.jsx: -------------------------------------------------------------------------------- 1 | import {STREAM_CONNECT, STREAM_SUBSCRIBE, STREAM_UNSUBSCRIBE} from '../types'; 2 | 3 | const initialState = { 4 | token: null 5 | }; 6 | 7 | export default (state = initialState, action = {}) => { 8 | switch (action.type) { 9 | case STREAM_CONNECT: 10 | return {...state, token: action.payload.token}; 11 | case STREAM_SUBSCRIBE: 12 | return { 13 | ...state, 14 | channels: [...(state.channels || []), ...action.payload.channels] 15 | }; 16 | case STREAM_UNSUBSCRIBE: 17 | return { 18 | ...state, 19 | channels: [ 20 | ...(state.channels || []).filter(c => action.payload.channels.indexOf(c) === -1) 21 | ] 22 | }; 23 | default: 24 | return state; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /webapp/store.jsx: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk'; 2 | import {createStore, applyMiddleware, compose} from 'redux'; 3 | 4 | import reducers from './reducers'; 5 | import sentry from './middleware/sentry'; 6 | import stream from './middleware/stream'; 7 | 8 | export default createStore( 9 | reducers, 10 | compose( 11 | applyMiddleware(thunk, sentry, stream), 12 | window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f 13 | ) 14 | ); 15 | -------------------------------------------------------------------------------- /webapp/utils/fileSize.jsx: -------------------------------------------------------------------------------- 1 | export const getSize = value => { 2 | if (value === 0) return '0 B'; 3 | if (!value) return null; 4 | 5 | let i = Math.max(Math.floor(Math.log(Math.abs(value)) / Math.log(1024)), 0); 6 | return ( 7 | (value / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i] 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /webapp/utils/media.jsx: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/core'; 2 | 3 | //github.com/import styled from '@emotion/styled';/import styled from '@emotion/styled';/blob/master/docs/tips-and-tricks.md 4 | const sizes = { 5 | lg: 1170, 6 | md: 992, 7 | sm: 768, 8 | xs: 376 9 | }; 10 | 11 | // iterate through the sizes and create a media template 12 | export default Object.keys(sizes).reduce((accumulator, label) => { 13 | // use em in breakpoints to work properly cross-browser and support users 14 | // changing their browsers font-size: https://zellwk.com/blog/media-query-units/ 15 | const emSize = sizes[label] / 16; 16 | accumulator[label] = (...args) => css` 17 | @media (max-width: ${emSize}em) { 18 | ${css(...args)}; 19 | } 20 | `; 21 | return accumulator; 22 | }, {}); 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | config/webpack.config.js -------------------------------------------------------------------------------- /zeus/api/authentication.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from zeus import auth 4 | from zeus.utils.sentry import span 5 | 6 | 7 | class HeaderAuthentication(object): 8 | @span("auth.header") 9 | def authenticate(self): 10 | return auth.get_tenant_from_headers(request.headers) 11 | 12 | 13 | class SessionAuthentication(object): 14 | @span("auth.session") 15 | def authenticate(self): 16 | user = auth.get_current_user() 17 | if not user: 18 | return None 19 | 20 | return auth.Tenant.from_user(user) 21 | -------------------------------------------------------------------------------- /zeus/api/controller.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from uuid import uuid4 3 | 4 | 5 | class Controller(Blueprint): 6 | def add_resource(self, path, cls): 7 | self.add_url_rule(path, view_func=cls.as_view(name=uuid4().hex)) 8 | -------------------------------------------------------------------------------- /zeus/api/resources/artifact_download.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, Response 2 | 3 | from zeus.models import Artifact 4 | 5 | from .base_artifact import BaseArtifactResource 6 | 7 | 8 | class ArtifactDownloadResource(BaseArtifactResource): 9 | def get(self, artifact: Artifact): 10 | """ 11 | Streams an artifact file to the client. 12 | """ 13 | return redirect(artifact.file.url_for(expire=30), code=302, Response=Response) 14 | -------------------------------------------------------------------------------- /zeus/api/resources/build_bundlestats.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import subqueryload_all 2 | 3 | from zeus.config import db 4 | from zeus.models import Job, Build, Bundle 5 | 6 | from .base_build import BaseBuildResource 7 | from ..schemas import BundleSchema 8 | 9 | bundle_schema = BundleSchema(many=True) 10 | 11 | 12 | class BuildBundleStatsResource(BaseBuildResource): 13 | def get(self, build: Build): 14 | """ 15 | Return bundle stats for a given build. 16 | """ 17 | job_ids = db.session.query(Job.id).filter(Job.build_id == build.id).subquery() 18 | 19 | query = ( 20 | Bundle.query.filter(Bundle.job_id.in_(job_ids)) 21 | .options(subqueryload_all(Bundle.assets)) 22 | .order_by(Bundle.name.asc()) 23 | ) 24 | 25 | return self.paginate_with_schema(bundle_schema, query) 26 | -------------------------------------------------------------------------------- /zeus/api/resources/build_diff.py: -------------------------------------------------------------------------------- 1 | from zeus.models import Build 2 | 3 | from .base_build import BaseBuildResource 4 | 5 | 6 | class BuildDiffResource(BaseBuildResource): 7 | def get(self, build: Build): 8 | """ 9 | Return a diff for the given build. 10 | """ 11 | if not build.revision: 12 | self.respond({"diff": None}) 13 | return self.respond({"diff": build.revision.generate_diff()}) 14 | -------------------------------------------------------------------------------- /zeus/api/resources/build_failures.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.func import array_agg_row 3 | from zeus.models import Build, FailureReason 4 | 5 | from .base_build import BaseBuildResource 6 | from ..schemas import AggregateFailureReasonSchema 7 | 8 | failurereasons_schema = AggregateFailureReasonSchema(many=True) 9 | 10 | 11 | class BuildFailuresResource(BaseBuildResource): 12 | def get(self, build: Build): 13 | """ 14 | Return a list of failure reasons for a given build. 15 | """ 16 | query = ( 17 | db.session.query( 18 | FailureReason.reason, 19 | array_agg_row(FailureReason.id, FailureReason.job_id).label("runs"), 20 | ) 21 | .filter(FailureReason.build_id == build.id) 22 | .group_by(FailureReason.reason) 23 | ) 24 | 25 | query = query.order_by(FailureReason.reason.asc()) 26 | 27 | return self.paginate_with_schema(failurereasons_schema, query) 28 | -------------------------------------------------------------------------------- /zeus/api/resources/catchall.py: -------------------------------------------------------------------------------- 1 | from .base import Resource 2 | 3 | 4 | class CatchallResource(Resource): 5 | def get(self, path=None): 6 | return self.not_found() 7 | 8 | post = get 9 | put = get 10 | delete = get 11 | patch = get 12 | -------------------------------------------------------------------------------- /zeus/api/resources/change_request_details.py: -------------------------------------------------------------------------------- 1 | from zeus.config import celery, db 2 | from zeus.models import ChangeRequest 3 | 4 | from .base_change_request import BaseChangeRequestResource 5 | from ..schemas import ChangeRequestSchema 6 | 7 | 8 | class ChangeRequestDetailsResource(BaseChangeRequestResource): 9 | def select_resource_for_update(self): 10 | return False 11 | 12 | def get(self, cr: ChangeRequest): 13 | schema = ChangeRequestSchema() 14 | return self.respond_with_schema(schema, cr) 15 | 16 | def put(self, cr: ChangeRequest): 17 | schema = ChangeRequestSchema( 18 | context={"repository": cr.repository, "change_request": cr} 19 | ) 20 | self.schema_from_request(schema, partial=True) 21 | if db.session.is_modified(cr): 22 | db.session.add(cr) 23 | db.session.commit() 24 | 25 | if not cr.parent_revision_sha or (not cr.head_revision_sha and cr.head_ref): 26 | celery.delay("zeus.resolve_ref_for_change_request", change_request_id=cr.id) 27 | 28 | return self.respond_with_schema(schema, cr) 29 | -------------------------------------------------------------------------------- /zeus/api/resources/github_organizations.py: -------------------------------------------------------------------------------- 1 | from zeus import auth 2 | from zeus.vcs.providers.github import GitHubRepositoryProvider 3 | 4 | from .base import Resource 5 | 6 | 7 | class GitHubOrganizationsResource(Resource): 8 | def get(self): 9 | """ 10 | Return a list of GitHub organizations avaiable to the current user. 11 | """ 12 | user = auth.get_current_user() 13 | provider = GitHubRepositoryProvider() 14 | return provider.get_owners(user) 15 | -------------------------------------------------------------------------------- /zeus/api/resources/hook_details.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.models import Hook 3 | 4 | from .base_hook import BaseHookResource 5 | from ..schemas import HookSchema 6 | 7 | hook_schema = HookSchema() 8 | 9 | 10 | class HookDetailsResource(BaseHookResource): 11 | def get(self, hook: Hook): 12 | """ 13 | Return a hook. 14 | """ 15 | return self.respond_with_schema(hook_schema, hook) 16 | 17 | def put(self, hook: Hook): 18 | """ 19 | Update a hook. 20 | """ 21 | hook_schema = HookSchema(context={"hook": hook}) 22 | self.schema_from_request(hook_schema, partial=True) 23 | if db.session.is_modified(hook): 24 | db.session.add(hook) 25 | db.session.commit() 26 | return self.respond_with_schema(hook_schema, hook) 27 | 28 | def delete(self, hook: Hook): 29 | """ 30 | Delete a hook. 31 | """ 32 | db.session.delete(hook) 33 | db.session.commit() 34 | return self.respond(status=204) 35 | -------------------------------------------------------------------------------- /zeus/api/resources/index.py: -------------------------------------------------------------------------------- 1 | from .base import Resource 2 | 3 | 4 | class IndexResource(Resource): 5 | def get(self): 6 | return {"version": 0} 7 | -------------------------------------------------------------------------------- /zeus/api/resources/repository_branches.py: -------------------------------------------------------------------------------- 1 | from zeus.models import Repository 2 | from zeus.vcs import vcs_client 3 | 4 | from .base_repository import BaseRepositoryResource 5 | 6 | 7 | class RepositoryBranchesResource(BaseRepositoryResource): 8 | cache_key = "api:1:repobranches:{repo_id}" 9 | cache_expire = 60 10 | 11 | def get(self, repo: Repository): 12 | """ 13 | Return a list of revisions for the given repository. 14 | """ 15 | result = vcs_client.branches(repo.id) 16 | 17 | return self.respond([{"name": r} for r in result]) 18 | -------------------------------------------------------------------------------- /zeus/api/resources/repository_index.py: -------------------------------------------------------------------------------- 1 | from zeus import auth 2 | from zeus.models import Repository 3 | 4 | from .base import Resource 5 | from ..schemas import RepositorySchema 6 | 7 | 8 | class RepositoryIndexResource(Resource): 9 | def get(self): 10 | """ 11 | Return a list of repositories. 12 | """ 13 | tenant = auth.get_current_tenant() 14 | if not tenant.repository_ids: 15 | return self.respond([]) 16 | 17 | query = ( 18 | Repository.query.filter(Repository.id.in_(tenant.repository_ids)) 19 | .order_by(Repository.owner_name.asc(), Repository.name.asc()) 20 | .limit(100) 21 | ) 22 | schema = RepositorySchema(many=True, context={"user": auth.get_current_user()}) 23 | return self.paginate_with_schema(schema, query) 24 | -------------------------------------------------------------------------------- /zeus/api/resources/revision_details.py: -------------------------------------------------------------------------------- 1 | from zeus.config import nplusone 2 | from zeus.models import Revision 3 | from zeus.utils.builds import fetch_build_for_revision 4 | 5 | from .base_revision import BaseRevisionResource 6 | from ..schemas import MetaBuildSchema 7 | 8 | build_schema = MetaBuildSchema(exclude=["repository"]) 9 | 10 | 11 | class RevisionDetailsResource(BaseRevisionResource): 12 | def select_resource_for_update(self) -> bool: 13 | return False 14 | 15 | # TODO(dcramer): this endpoint should be returning a revision, but is 16 | # instead returning a build 17 | def get(self, revision: Revision, repo=None): 18 | """ 19 | Return the joined build status of a revision. 20 | """ 21 | with nplusone.ignore("eager_load"): 22 | build = fetch_build_for_revision(revision) 23 | if not build: 24 | return self.respond(status=404) 25 | 26 | return self.respond_with_schema(build_schema, build) 27 | -------------------------------------------------------------------------------- /zeus/api/resources/revision_diff.py: -------------------------------------------------------------------------------- 1 | from zeus.models import Revision 2 | 3 | from .base_revision import BaseRevisionResource 4 | 5 | 6 | class RevisionDiffResource(BaseRevisionResource): 7 | def get(self, revision: Revision): 8 | """ 9 | Return a diff for the given revision. 10 | """ 11 | if not revision: 12 | self.respond({"diff": None}) 13 | return self.respond({"diff": revision.generate_diff()}) 14 | -------------------------------------------------------------------------------- /zeus/api/resources/revision_jobs.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import subqueryload_all 2 | 3 | from zeus.models import Job, Revision 4 | from zeus.utils.builds import fetch_build_for_revision 5 | 6 | from .base_revision import BaseRevisionResource 7 | from ..schemas import JobSchema 8 | 9 | job_schema = JobSchema() 10 | jobs_schema = JobSchema(many=True) 11 | 12 | 13 | class RevisionJobsResource(BaseRevisionResource): 14 | def get(self, revision: Revision): 15 | """ 16 | Return a list of jobs for a given revision. 17 | """ 18 | build = fetch_build_for_revision(revision) 19 | if not build: 20 | return self.respond(status=404) 21 | 22 | build_ids = [original.id for original in build.original] 23 | query = ( 24 | Job.query.options(subqueryload_all("stats"), subqueryload_all("failures")) 25 | .filter(Job.build_id.in_(build_ids)) 26 | .order_by(Job.number.asc()) 27 | ) 28 | return self.respond_with_schema(jobs_schema, query) 29 | -------------------------------------------------------------------------------- /zeus/api/resources/test_details.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import joinedload, undefer 2 | 3 | from zeus.models import TestCase 4 | 5 | from .base import Resource 6 | from ..schemas import TestCaseSchema 7 | 8 | 9 | class TestDetailsResource(Resource): 10 | def select_resource_for_update(self) -> bool: 11 | return False 12 | 13 | def get(self, test_id: str): 14 | testcase = TestCase.query.options(undefer("message"), joinedload("job")).get( 15 | test_id 16 | ) 17 | 18 | schema = TestCaseSchema() 19 | return self.respond_with_schema(schema, testcase) 20 | -------------------------------------------------------------------------------- /zeus/api/resources/user_emails.py: -------------------------------------------------------------------------------- 1 | from zeus import auth 2 | from zeus.models import Email 3 | 4 | from .base import Resource 5 | from ..schemas import EmailSchema 6 | 7 | emails_schema = EmailSchema(many=True) 8 | 9 | 10 | class UserEmailsResource(Resource): 11 | def get(self, user_id): 12 | """ 13 | Return a list of builds for the given user. 14 | """ 15 | if user_id == "me": 16 | user = auth.get_current_user() 17 | if not user: 18 | return self.error("not authenticated", 401) 19 | 20 | else: 21 | raise NotImplementedError 22 | 23 | query = Email.query.filter(Email.user_id == user.id).order_by(Email.email.asc()) 24 | return self.paginate_with_schema(emails_schema, query) 25 | -------------------------------------------------------------------------------- /zeus/api/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .artifact import * # NOQA 2 | from .author import * # NOQA 3 | from .build import * # NOQA 4 | from .bundlestat import * # NOQA 5 | from .change_request import * # NOQA 6 | from .email import * # NOQA 7 | from .failurereason import * # NOQA 8 | from .filecoverage import * # NOQA 9 | from .hook import * # NOQA 10 | from .identity import * # NOQA 11 | from .job import * # NOQA 12 | from .pending_artifact import * # NOQA 13 | from .repository import * # NOQA 14 | from .revision import * # NOQA 15 | from .stats import * # NOQA 16 | from .styleviolation import * # NOQA 17 | from .testcase import * # NOQA 18 | from .testcase_rollup import * # NOQA 19 | from .token import * # NOQA 20 | from .user import * # NOQA 21 | -------------------------------------------------------------------------------- /zeus/api/schemas/author.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class AuthorSchema(Schema): 5 | id = fields.UUID(dump_only=True) 6 | name = fields.String() 7 | email = fields.String(required=False) 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/bundlestat.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class BundleAssetSchema(Schema): 5 | name = fields.Str(required=True) 6 | size = fields.Int() 7 | 8 | 9 | class BundleSchema(Schema): 10 | id = fields.UUID(dump_only=True) 11 | job_id = fields.UUID(dump_only=True) 12 | name = fields.Str(required=True) 13 | assets = fields.List(fields.Nested(BundleAssetSchema)) 14 | -------------------------------------------------------------------------------- /zeus/api/schemas/email.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class EmailSchema(Schema): 5 | id = fields.UUID(dump_only=True) 6 | email = fields.Str() 7 | verified = fields.Bool(default=False) 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/failurereason.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields, pre_dump 2 | from uuid import UUID 3 | 4 | from zeus.api.schemas.fields.enum import EnumField 5 | from zeus.models import FailureReason 6 | 7 | 8 | class ReasonField(EnumField): 9 | enum = FailureReason.Reason 10 | 11 | 12 | class FailureReasonSchema(Schema): 13 | reason = ReasonField() 14 | 15 | 16 | class ExecutionSchema(Schema): 17 | id = fields.UUID(dump_only=True) 18 | job_id = fields.UUID(required=True) 19 | 20 | 21 | class AggregateFailureReasonSchema(Schema): 22 | reason = ReasonField() 23 | runs = fields.List(fields.Nested(ExecutionSchema), required=True) 24 | 25 | @pre_dump 26 | def process_aggregates(self, data, **kwargs): 27 | return { 28 | "reason": data.reason, 29 | "runs": [ 30 | {"id": UUID(e[0]), "job_id": UUID(e[1]) if e[1] else None} 31 | for e in sorted( 32 | data.runs, key=lambda e: UUID(e[1]) if e[1] else 0, reverse=True 33 | ) 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from .enum import * # NOQA 2 | from .file import * # NOQA 3 | from .permission import * # NOQA 4 | from .result import * # NOQA 5 | from .revision import * # NOQA 6 | from .severity import * # NOQA 7 | from .status import * # NOQA 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/enum.py: -------------------------------------------------------------------------------- 1 | from marshmallow.fields import Field 2 | from marshmallow.exceptions import ValidationError 3 | 4 | 5 | class EnumField(Field): 6 | enum = None 7 | 8 | def __init__(self, enum=None, *args, **kwargs): 9 | if enum is not None: 10 | self.enum = enum 11 | elif self.enum is None: 12 | raise ValueError("`enum` must be specified") 13 | 14 | Field.__init__(self, *args, **kwargs) 15 | 16 | def _serialize(self, value, attr, obj, **kwargs): 17 | if value is None: 18 | return None 19 | 20 | return value.name 21 | 22 | def _deserialize(self, value, attr, data, **kwargs): 23 | if value is None: 24 | return None 25 | 26 | if isinstance(value, self.enum): 27 | return value 28 | 29 | try: 30 | return self.enum[value] 31 | 32 | except KeyError: 33 | raise ValidationError("Not a valid choice.") 34 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/file.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields 2 | 3 | 4 | class FileField(fields.Field): 5 | def _serialize(self, value, attr, obj, **kwargs): 6 | if value is None: 7 | return None 8 | 9 | elif isinstance(value, dict): 10 | return value 11 | 12 | return {"name": value.filename, "size": value.size} 13 | 14 | def _deserialize(self, value, attr, data, **kwargs): 15 | # XXX(dcramer): this would need to serialize into something compatible with 16 | # the schema 17 | return None 18 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/permission.py: -------------------------------------------------------------------------------- 1 | from zeus.constants import Permission 2 | 3 | from .enum import EnumField 4 | 5 | 6 | class PermissionField(EnumField): 7 | enum = Permission 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/result.py: -------------------------------------------------------------------------------- 1 | from zeus.constants import Result 2 | 3 | from .enum import EnumField 4 | 5 | 6 | class ResultField(EnumField): 7 | enum = Result 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/severity.py: -------------------------------------------------------------------------------- 1 | from zeus.constants import Severity 2 | 3 | from .enum import EnumField 4 | 5 | 6 | class SeverityField(EnumField): 7 | enum = Severity 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/fields/status.py: -------------------------------------------------------------------------------- 1 | from zeus.constants import Status 2 | 3 | from .enum import EnumField 4 | 5 | 6 | class StatusField(EnumField): 7 | enum = Status 8 | -------------------------------------------------------------------------------- /zeus/api/schemas/filecoverage.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class FileCoverageSchema(Schema): 5 | filename = fields.Str() 6 | data = fields.Str() 7 | lines_covered = fields.Number() 8 | lines_uncovered = fields.Number() 9 | diff_lines_covered = fields.Number() 10 | diff_lines_uncovered = fields.Number() 11 | -------------------------------------------------------------------------------- /zeus/api/schemas/identity.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class IdentitySchema(Schema): 5 | id = fields.UUID(dump_only=True) 6 | external_id = fields.Str(dump_only=True) 7 | provider = fields.Str(dump_only=True) 8 | scopes = fields.List(fields.Str, dump_only=True) 9 | created_at = fields.DateTime(attribute="date_created", dump_only=True) 10 | -------------------------------------------------------------------------------- /zeus/api/schemas/pending_artifact.py: -------------------------------------------------------------------------------- 1 | # from werkzeug.datastructures import FileStorage 2 | 3 | from marshmallow import Schema, fields, post_load 4 | 5 | from zeus.models import PendingArtifact 6 | 7 | from .fields import FileField 8 | 9 | 10 | class PendingArtifactSchema(Schema): 11 | id = fields.UUID(dump_only=True) 12 | provider = fields.Str() 13 | external_build_id = fields.Str() 14 | external_job_id = fields.Str() 15 | hook_id = fields.Str() 16 | # name can be inferred from file 17 | name = fields.Str(required=False) 18 | type = fields.Str(required=False) 19 | # XXX(dcramer): cant find a way to get marshmallow to handle request.files 20 | file = FileField(required=False) 21 | 22 | @post_load(pass_many=False) 23 | def build_instance(self, data, **kwargs): 24 | return PendingArtifact(**data) 25 | -------------------------------------------------------------------------------- /zeus/api/schemas/revision.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | from .author import AuthorSchema 4 | 5 | 6 | class RevisionSchema(Schema): 7 | id = fields.UUID(dump_only=True) 8 | sha = fields.Str() 9 | message = fields.Str() 10 | authors = fields.List(fields.Nested(AuthorSchema()), dump_only=True) 11 | created_at = fields.DateTime(attribute="date_created", dump_only=True) 12 | committed_at = fields.DateTime(attribute="date_committed", dump_only=True) 13 | -------------------------------------------------------------------------------- /zeus/api/schemas/styleviolation.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | from .fields import SeverityField 4 | 5 | 6 | class StyleViolationSchema(Schema): 7 | id = fields.UUID(dump_only=True) 8 | job_id = fields.UUID(dump_only=True) 9 | filename = fields.Str(required=True) 10 | severity = SeverityField(required=True) 11 | message = fields.Str(required=True) 12 | lineno = fields.Number(required=False) 13 | colno = fields.Number(required=False) 14 | source = fields.Str(required=False) 15 | -------------------------------------------------------------------------------- /zeus/api/schemas/testcase_rollup.py: -------------------------------------------------------------------------------- 1 | __all__ = ("TestCaseStatisticsSchema",) 2 | 3 | from marshmallow import Schema, fields 4 | 5 | 6 | class TestCaseStatisticsSchema(Schema): 7 | id = fields.UUID(dump_only=True) 8 | name = fields.Str(required=True) 9 | hash = fields.Str(dump_only=True) 10 | 11 | runs_failed = fields.Integer(dump_only=True) 12 | total_runs = fields.Integer(dump_only=True) 13 | avg_duration = fields.Integer(dump_only=True) 14 | -------------------------------------------------------------------------------- /zeus/api/schemas/token.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class TokenSchema(Schema): 5 | id = fields.UUID(dump_only=True) 6 | key = fields.Str(dump_only=True) 7 | -------------------------------------------------------------------------------- /zeus/api/schemas/user.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from marshmallow import Schema, fields, pre_dump, validate 3 | 4 | 5 | class MailOptionsSchema(Schema): 6 | notify_author = fields.String(value=validate.ContainsOnly(["1", "0"]), missing="1") 7 | 8 | 9 | class UserOptionsSchema(Schema): 10 | mail = fields.Nested(MailOptionsSchema) 11 | 12 | @pre_dump 13 | def process_options(self, data, **kwargs): 14 | result = defaultdict(lambda: defaultdict(int)) 15 | result["mail"]["notify_author"] = "1" 16 | for option in data: 17 | bits = option.name.split(".", 1) 18 | if len(bits) != 2: 19 | continue 20 | 21 | result[bits[0]][bits[1]] = option.value 22 | return result 23 | 24 | 25 | class UserSchema(Schema): 26 | id = fields.UUID(dump_only=True) 27 | email = fields.Str(dump_only=True) 28 | options = fields.Nested(UserOptionsSchema) 29 | created_at = fields.DateTime(attribute="date_created", dump_only=True) 30 | -------------------------------------------------------------------------------- /zeus/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/api/utils/__init__.py -------------------------------------------------------------------------------- /zeus/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file acts as a default entry point for app creation. 3 | """ 4 | 5 | from .config import celery, create_app 6 | 7 | app = create_app() 8 | 9 | celery = celery.get_celery_app() 10 | -------------------------------------------------------------------------------- /zeus/artifacts/base.py: -------------------------------------------------------------------------------- 1 | from typing import FrozenSet 2 | 3 | 4 | class ArtifactHandler(object): 5 | supported_types: FrozenSet[str] = frozenset() 6 | 7 | def __init__(self, job): 8 | self.job = job 9 | -------------------------------------------------------------------------------- /zeus/cli/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .auth import * # NOQA 4 | from .cleanup import * # NOQA 5 | from .devserver import * # NOQA 6 | from .hooks import * # NOQA 7 | from .init import * # NOQA 8 | from .mocks import * # NOQA 9 | from .pubsub import * # NOQA 10 | from .repos import * # NOQA 11 | from .shell import * # NOQA 12 | from .ssh_connect import * # NOQA 13 | from .vcs_server import * # NOQA 14 | from .web import * # NOQA 15 | from .worker import * # NOQA 16 | 17 | 18 | def main(): 19 | import os 20 | 21 | os.environ.setdefault("PYTHONUNBUFFERED", "true") 22 | os.environ.setdefault("FLASK_APP", "zeus.app") 23 | 24 | from .base import cli 25 | 26 | cli.main() 27 | -------------------------------------------------------------------------------- /zeus/cli/auth.py: -------------------------------------------------------------------------------- 1 | import click 2 | import zeus.auth 3 | 4 | from zeus.models import User 5 | 6 | from .base import cli 7 | 8 | 9 | @cli.group("auth") 10 | def auth(): 11 | pass 12 | 13 | 14 | @auth.command() 15 | @click.argument("email", required=True) 16 | def generate_token(email): 17 | user = User.query.filter(User.email == email).first() 18 | tenant = zeus.auth.Tenant.from_user(user) 19 | token = zeus.auth.generate_token(tenant) 20 | print('Authentication for "%s"' % user.email) 21 | print("User ID") 22 | print(" %s " % str(user.id)) 23 | print("Email") 24 | print(" %s " % user.email) 25 | print("Token") 26 | print(" %s " % token.decode("utf-8")) 27 | -------------------------------------------------------------------------------- /zeus/cli/base.py: -------------------------------------------------------------------------------- 1 | from flask.cli import FlaskGroup 2 | 3 | cli = FlaskGroup(help="zeus") 4 | -------------------------------------------------------------------------------- /zeus/cli/pubsub.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import click 3 | import os 4 | 5 | from flask import current_app 6 | 7 | from zeus.pubsub.server import build_server 8 | 9 | from .base import cli 10 | 11 | 12 | @cli.command() 13 | @click.option("--host", default="0.0.0.0") 14 | @click.option("--port", default=8090, type=int) 15 | def pubsub(host, port): 16 | os.environ["PYTHONUNBUFFERED"] = "true" 17 | 18 | import logging 19 | 20 | current_app.logger.setLevel(logging.DEBUG) 21 | 22 | loop = asyncio.get_event_loop() 23 | loop.run_until_complete(build_server(loop, host, port)) 24 | print("pubsub running on http://{}:{}".format(host, port)) 25 | 26 | try: 27 | loop.run_forever() 28 | except KeyboardInterrupt: 29 | print("Shutting Down!") 30 | loop.close() 31 | -------------------------------------------------------------------------------- /zeus/cli/shell.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .base import cli 4 | 5 | 6 | @cli.command() 7 | def shell(): 8 | import IPython 9 | from zeus.app import app 10 | from zeus.config import db 11 | 12 | ctx = app.make_shell_context() 13 | ctx["app"].test_request_context().push() 14 | ctx["db"] = db 15 | # Import all models into the shell context 16 | ctx.update(db.Model._decl_class_registry) 17 | 18 | startup = os.environ.get("PYTHONSTARTUP") 19 | if startup and os.path.isfile(startup): 20 | with open(startup, "rb") as f: 21 | eval(compile(f.read(), startup, "exec"), ctx) 22 | 23 | IPython.start_ipython(user_ns=ctx, argv=[]) 24 | -------------------------------------------------------------------------------- /zeus/cli/vcs_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import click 3 | import os 4 | 5 | from flask import current_app 6 | 7 | from zeus.vcs.server import build_server 8 | 9 | from .base import cli 10 | 11 | 12 | @cli.command() 13 | @click.option("--host", default="0.0.0.0") 14 | @click.option("--port", default=8070, type=int) 15 | def vcs_server(host, port): 16 | os.environ["PYTHONUNBUFFERED"] = "true" 17 | 18 | import logging 19 | 20 | current_app.logger.setLevel(logging.DEBUG) 21 | 22 | loop = asyncio.get_event_loop() 23 | loop.run_until_complete(build_server(loop, host, port)) 24 | print("vcs-server running on http://{}:{}".format(host, port)) 25 | 26 | try: 27 | loop.run_forever() 28 | except KeyboardInterrupt: 29 | print("Shutting Down!") 30 | loop.close() 31 | -------------------------------------------------------------------------------- /zeus/cli/worker.py: -------------------------------------------------------------------------------- 1 | import click 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | from .base import cli 7 | 8 | 9 | @cli.command() 10 | @click.option("--log-level", "-l", default="INFO") 11 | @click.option("--cron/--no-cron", default=True) 12 | def worker(cron, log_level): 13 | command = [ 14 | "celery", 15 | "--app=zeus.app:celery", 16 | "worker", 17 | "--loglevel={}".format(log_level), 18 | # TODO(dcramer): which of these can we kill? 19 | # '--without-mingle', 20 | # '--without-gossip', 21 | # '--without-heartbeat', 22 | "--max-tasks-per-child=10000", 23 | ] 24 | if cron: 25 | command.extend(["--beat", "--scheduler=redbeat.RedBeatScheduler"]) 26 | 27 | sys.exit( 28 | subprocess.call( 29 | command, 30 | cwd=os.getcwd(), 31 | env=os.environ, 32 | stdout=sys.stdout, 33 | stderr=sys.stderr, 34 | ) 35 | ) 36 | -------------------------------------------------------------------------------- /zeus/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/db/__init__.py -------------------------------------------------------------------------------- /zeus/db/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .enum import * # NOQA 2 | from .file import * # NOQA 3 | from .guid import * # NOQA 4 | from .json import * # NOQA 5 | -------------------------------------------------------------------------------- /zeus/db/types/enum.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Enum", "IntEnum", "StrEnum"] 2 | 3 | from enum import Enum as EnumType 4 | from typing import Optional, Type 5 | 6 | from sqlalchemy.types import TypeDecorator, INT, STRINGTYPE 7 | 8 | 9 | class Enum(TypeDecorator): 10 | impl = INT 11 | 12 | def __init__(self, enum: Optional[Type[EnumType]] = None, *args, **kwargs): 13 | self.enum = enum 14 | super(Enum, self).__init__(*args, **kwargs) 15 | 16 | def process_bind_param(self, value, dialect): 17 | if value is None: 18 | return value 19 | 20 | return value.value 21 | 22 | def process_result_value(self, value, dialect): 23 | if value is None: 24 | return value 25 | 26 | elif self.enum: 27 | return self.enum(value) 28 | 29 | return value 30 | 31 | 32 | class IntEnum(Enum): 33 | pass 34 | 35 | 36 | class StrEnum(Enum): 37 | impl = STRINGTYPE 38 | -------------------------------------------------------------------------------- /zeus/factories/__init__.py: -------------------------------------------------------------------------------- 1 | from .api_token import * # NOQA 2 | from .artifact import * # NOQA 3 | from .author import * # NOQA 4 | from .base import * # NOQA 5 | from .build import * # NOQA 6 | from .bundlestat import * # NOQA 7 | from .change_request import * # NOQA 8 | from .email import * # NOQA 9 | from .failurereason import * # NOQA 10 | from .filecoverage import * # NOQA 11 | from .hook import * # NOQA 12 | from .identity import * # NOQA 13 | from .job import * # NOQA 14 | from .pending_artifact import * # NOQA 15 | from .repository import * # NOQA 16 | from .revision import * # NOQA 17 | from .styleviolation import * # NOQA 18 | from .testcase import * # NOQA 19 | from .testcase_rollup import * # NOQA 20 | from .user import * # NOQA 21 | -------------------------------------------------------------------------------- /zeus/factories/api_token.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from datetime import timedelta 4 | 5 | from zeus import models 6 | from zeus.utils import timezone 7 | 8 | from .base import ModelFactory 9 | from .types import GUIDFactory 10 | 11 | 12 | class ApiTokenFactory(ModelFactory): 13 | id = GUIDFactory() 14 | 15 | class Meta: 16 | model = models.ApiToken 17 | 18 | class Params: 19 | expired = factory.Trait( 20 | expires_at=factory.LazyAttribute( 21 | lambda o: timezone.now() - timedelta(days=1) 22 | ) 23 | ) 24 | -------------------------------------------------------------------------------- /zeus/factories/author.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.faker 3 | 4 | from zeus import models 5 | 6 | from .base import ModelFactory 7 | 8 | 9 | class AuthorFactory(ModelFactory): 10 | repository = factory.SubFactory("zeus.factories.RepositoryFactory") 11 | repository_id = factory.SelfAttribute("repository.id") 12 | name = factory.Faker("first_name") 13 | email = factory.Faker("email") 14 | 15 | class Meta: 16 | model = models.Author 17 | -------------------------------------------------------------------------------- /zeus/factories/email.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from zeus import models 4 | 5 | from .base import ModelFactory 6 | from .types import GUIDFactory 7 | 8 | 9 | class EmailFactory(ModelFactory): 10 | id = GUIDFactory() 11 | user = factory.SubFactory("zeus.factories.UserFactory") 12 | user_id = factory.SelfAttribute("user.id") 13 | email = factory.Faker("email") 14 | verified = True 15 | 16 | class Meta: 17 | model = models.Email 18 | -------------------------------------------------------------------------------- /zeus/factories/hook.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from zeus import models 4 | from zeus.utils import timezone 5 | 6 | from .base import ModelFactory 7 | from .types import GUIDFactory 8 | 9 | 10 | class HookFactory(ModelFactory): 11 | id = GUIDFactory() 12 | repository = factory.SubFactory("zeus.factories.RepositoryFactory") 13 | repository_id = factory.SelfAttribute("repository.id") 14 | provider = "travis" 15 | config = factory.LazyAttribute(lambda x: {"domain": "api.travis-ci.com"}) 16 | date_created = factory.LazyAttribute(lambda o: timezone.now()) 17 | 18 | class Meta: 19 | model = models.Hook 20 | 21 | class Params: 22 | travis_com = factory.Trait( 23 | provider="travis", config={"domain": "api.travis-ci.com"} 24 | ) 25 | travis_org = factory.Trait( 26 | provider="travis", config={"domain": "api.travis-ci.org"} 27 | ) 28 | -------------------------------------------------------------------------------- /zeus/factories/identity.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from zeus import models 4 | 5 | from .base import ModelFactory 6 | from .types import GUIDFactory 7 | 8 | 9 | class IdentityFactory(ModelFactory): 10 | id = GUIDFactory() 11 | user = factory.SubFactory("zeus.factories.UserFactory") 12 | user_id = factory.SelfAttribute("user.id") 13 | external_id = factory.Faker("email") 14 | 15 | class Meta: 16 | model = models.Identity 17 | 18 | class Params: 19 | github = factory.Trait( 20 | provider="github", 21 | scopes=["user:email", "read:org", "repo"], 22 | config={ 23 | "access_token": "access-token", 24 | "refresh_token": "refresh-token", 25 | "login": "test", 26 | }, 27 | ) 28 | github_barebones = factory.Trait( 29 | provider="github", 30 | scopes=["user:email", "read:org"], 31 | config={ 32 | "access_token": "access-token", 33 | "refresh_token": "refresh-token", 34 | "login": "test", 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /zeus/factories/revision.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.faker 3 | 4 | from datetime import timedelta 5 | from faker import Factory 6 | 7 | faker = Factory.create() 8 | 9 | from zeus import models 10 | from zeus.config import db 11 | from zeus.utils import timezone 12 | 13 | from .base import ModelFactory 14 | 15 | 16 | class RevisionFactory(ModelFactory): 17 | sha = factory.Faker("sha1") 18 | repository = factory.SubFactory("zeus.factories.RepositoryFactory") 19 | repository_id = factory.SelfAttribute("repository.id") 20 | message = factory.LazyAttribute( 21 | lambda o: "{}\n\n{}".format(faker.sentence(), faker.sentence()) 22 | ) 23 | date_created = factory.LazyAttribute( 24 | lambda o: timezone.now() - timedelta(minutes=30) 25 | ) 26 | 27 | @factory.post_generation 28 | def authors(self, create, extracted, **kwargs): 29 | if not create: 30 | return 31 | 32 | if extracted: 33 | self.authors = extracted 34 | 35 | db.session.flush() 36 | 37 | class Meta: 38 | model = models.Revision 39 | -------------------------------------------------------------------------------- /zeus/factories/testcase.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.fuzzy 3 | 4 | from faker import Factory 5 | 6 | faker = Factory.create() 7 | 8 | from zeus import models 9 | from zeus.constants import Result 10 | 11 | from .base import ModelFactory 12 | from .types import GUIDFactory 13 | 14 | 15 | class TestCaseFactory(ModelFactory): 16 | id = GUIDFactory() 17 | name = factory.LazyAttribute( 18 | lambda o: "tests.%s.%s.%s" % (faker.word(), faker.word(), faker.word()) 19 | ) 20 | job = factory.SubFactory("zeus.factories.JobFactory") 21 | job_id = factory.SelfAttribute("job.id") 22 | repository = factory.SelfAttribute("job.repository") 23 | repository_id = factory.SelfAttribute("repository.id") 24 | result = factory.Iterator([Result.failed, Result.passed]) 25 | duration = factory.Faker("random_int", min=1, max=100000) 26 | message = factory.faker.Faker("sentence") 27 | 28 | class Meta: 29 | model = models.TestCase 30 | 31 | class Params: 32 | failed = factory.Trait(result=Result.failed, message="A failure occurred") 33 | passed = factory.Trait(result=Result.passed) 34 | -------------------------------------------------------------------------------- /zeus/factories/testcase_rollup.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.fuzzy 3 | 4 | from faker import Factory 5 | 6 | faker = Factory.create() 7 | 8 | from zeus import models 9 | 10 | from .base import ModelFactory 11 | from .types import GUIDFactory 12 | 13 | 14 | class TestCaseRollupFactory(ModelFactory): 15 | id = GUIDFactory() 16 | name = factory.LazyAttribute( 17 | lambda o: "tests.%s.%s.%s" % (faker.word(), faker.word(), faker.word()) 18 | ) 19 | repository = factory.SubFactory("zeus.factories.RepositoryFactory") 20 | repository_id = factory.SelfAttribute("repository.id") 21 | total_runs = factory.LazyAttribute(lambda o: o.runs_passed + o.runs_failed) 22 | total_duration = factory.Faker("random_int", min=1, max=100000) 23 | runs_passed = factory.Faker("random_int", min=1, max=100) 24 | runs_failed = factory.Faker("random_int", min=1, max=100) 25 | 26 | class Meta: 27 | model = models.TestCaseRollup 28 | -------------------------------------------------------------------------------- /zeus/factories/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .guid import * # NOQA 2 | -------------------------------------------------------------------------------- /zeus/factories/types/guid.py: -------------------------------------------------------------------------------- 1 | from factory import LazyFunction 2 | 3 | from zeus.db.types import GUID 4 | 5 | 6 | class GUIDFactory(LazyFunction): 7 | def __init__(self, *args, **kwargs): 8 | LazyFunction.__init__(self, GUID.default_value, *args, **kwargs) 9 | -------------------------------------------------------------------------------- /zeus/factories/user.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from zeus import models 4 | from zeus.utils import timezone 5 | 6 | from .base import ModelFactory 7 | from .types import GUIDFactory 8 | 9 | 10 | class UserFactory(ModelFactory): 11 | id = GUIDFactory() 12 | email = factory.Faker("email") 13 | date_created = factory.LazyAttribute(lambda o: timezone.now()) 14 | date_active = factory.LazyAttribute(lambda o: o.date_created) 15 | 16 | class Meta: 17 | model = models.User 18 | -------------------------------------------------------------------------------- /zeus/migrations/059fd6da96ee_cleanup_repo_access.py: -------------------------------------------------------------------------------- 1 | """cleanup_repo_access 2 | 3 | Revision ID: 059fd6da96ee 4 | Revises: e2f926a2b0fe 5 | Create Date: 2018-12-19 11:31:47.040286 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "059fd6da96ee" 14 | down_revision = "e2f926a2b0fe" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_index("ix_repository_access_repository_id", table_name="repository_access") 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.create_index( 28 | "ix_repository_access_repository_id", 29 | "repository_access", 30 | ["repository_id"], 31 | unique=False, 32 | ) 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /zeus/migrations/070ac78ddbcb_optional_label.py: -------------------------------------------------------------------------------- 1 | """optional_label 2 | 3 | Revision ID: 070ac78ddbcb 4 | Revises: 8536b0fcf0a2 5 | Create Date: 2019-11-25 10:46:05.502420 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "070ac78ddbcb" 14 | down_revision = "8536b0fcf0a2" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column("build", "label", existing_type=sa.VARCHAR(), nullable=True) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.alter_column("build", "label", existing_type=sa.VARCHAR(), nullable=False) 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /zeus/migrations/16414a5b4ed9_backfill_authors.py: -------------------------------------------------------------------------------- 1 | """backfill_authors 2 | 3 | Revision ID: 16414a5b4ed9 4 | Revises: 56684708bb21 5 | Create Date: 2020-01-07 16:21:35.504437 6 | 7 | """ 8 | import zeus 9 | from alembic import op 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "16414a5b4ed9" 15 | down_revision = "56684708bb21" 16 | branch_labels = () 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | connection = op.get_bind() 22 | 23 | connection.execute( 24 | """ 25 | INSERT INTO build_author (build_id, author_id) 26 | SELECT id, author_id FROM build 27 | WHERE author_id IS NOT NULL 28 | ON CONFLICT DO NOTHING 29 | """ 30 | ) 31 | 32 | connection.execute( 33 | """ 34 | INSERT INTO revision_author (repository_id, revision_sha, author_id) 35 | SELECT repository_id, sha, author_id FROM revision 36 | WHERE author_id IS NOT NULL 37 | ON CONFLICT DO NOTHING 38 | """ 39 | ) 40 | 41 | 42 | def downgrade(): 43 | pass 44 | -------------------------------------------------------------------------------- /zeus/migrations/1782e8a9f689_scheduled_repo_updates.py: -------------------------------------------------------------------------------- 1 | """scheduled_repo_updates 2 | 3 | Revision ID: 1782e8a9f689 4 | Revises: 0f81e9efc84a 5 | Create Date: 2019-10-24 23:24:34.861892 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "1782e8a9f689" 14 | down_revision = "0f81e9efc84a" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "repository", 23 | sa.Column("next_update", sa.TIMESTAMP(timezone=True), nullable=True), 24 | ) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_column("repository", "next_update") 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /zeus/migrations/33da83c61635_optimize_origin_index.py: -------------------------------------------------------------------------------- 1 | """optimize_origin_index 2 | 3 | Revision ID: 33da83c61635 4 | Revises: cd1324dcb6ba 5 | Create Date: 2019-01-23 12:47:40.304286 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "33da83c61635" 14 | down_revision = "cd1324dcb6ba" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_index( 22 | "idx_build_outcomes", 23 | "build", 24 | ["status", "result", "date_created"], 25 | unique=False, 26 | ) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_index("idx_build_outcomes", table_name="build") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /zeus/migrations/340d5cc7e806_index_artifacts.py: -------------------------------------------------------------------------------- 1 | """index_artifacts 2 | 3 | Revision ID: 340d5cc7e806 4 | Revises: af3f4bdc27d1 5 | Create Date: 2019-08-09 12:37:50.706914 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "340d5cc7e806" 14 | down_revision = "af3f4bdc27d1" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_index( 22 | "idx_artifact_job", "artifact", ["repository_id", "job_id"], unique=False 23 | ) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_index("idx_artifact_job", table_name="artifact") 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /zeus/migrations/355fb6ea34f8_job_allow_failures.py: -------------------------------------------------------------------------------- 1 | """job_allow_failures 2 | 3 | Revision ID: 355fb6ea34f8 4 | Revises: 53c5cd5b170f 5 | Create Date: 2017-09-29 13:27:31.861345 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "355fb6ea34f8" 14 | down_revision = "53c5cd5b170f" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "job", 23 | sa.Column("allow_failure", sa.Boolean(), server_default="0", nullable=False), 24 | ) 25 | 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("job", "allow_failure") 33 | 34 | 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /zeus/migrations/392eb568af84_index_finished_jobs.py: -------------------------------------------------------------------------------- 1 | """index_finished_jobs 2 | 3 | Revision ID: 392eb568af84 4 | Revises: e373a7bffa18 5 | Create Date: 2020-04-22 12:59:25.815399 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "392eb568af84" 14 | down_revision = "e373a7bffa18" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_index( 22 | "idx_job_finished", 23 | "job", 24 | ["repository_id", "status", "date_finished"], 25 | unique=False, 26 | ) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_index("idx_job_finished", table_name="job") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /zeus/migrations/4b3c2ca23454_identity_scopes.py: -------------------------------------------------------------------------------- 1 | """identity_scopes 2 | 3 | Revision ID: 4b3c2ca23454 4 | Revises: c257bd5a6236 5 | Create Date: 2017-10-11 11:40:44.554528 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "4b3c2ca23454" 14 | down_revision = "c257bd5a6236" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "identity", 23 | sa.Column("scopes", postgresql.ARRAY(sa.String(length=64)), nullable=True), 24 | ) 25 | 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("identity", "scopes") 33 | 34 | 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /zeus/migrations/52a5d85ba249_backfill_cr_multi_author.py: -------------------------------------------------------------------------------- 1 | """backfill_cr_multi_author 2 | 3 | Revision ID: 52a5d85ba249 4 | Revises: 70383b887d4a 5 | Create Date: 2020-03-04 15:23:10.842507 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "52a5d85ba249" 14 | down_revision = "70383b887d4a" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | connection = op.get_bind() 21 | 22 | connection.execute( 23 | """ 24 | INSERT INTO change_request_author (change_request_id, author_id) 25 | SELECT id, author_id FROM change_request 26 | WHERE author_id IS NOT NULL 27 | ON CONFLICT DO NOTHING 28 | """ 29 | ) 30 | 31 | 32 | def downgrade(): 33 | pass 34 | -------------------------------------------------------------------------------- /zeus/migrations/53c5cd5b170f_artifact_status.py: -------------------------------------------------------------------------------- 1 | """artifact_status 2 | 3 | Revision ID: 53c5cd5b170f 4 | Revises: 9dbc97018a55 5 | Create Date: 2017-08-28 16:12:53.035600 6 | 7 | """ 8 | import zeus 9 | from alembic import op 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "53c5cd5b170f" 15 | down_revision = "9dbc97018a55" 16 | branch_labels = () 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column( 23 | "artifact", 24 | sa.Column( 25 | "status", 26 | zeus.db.types.enum.Enum(), 27 | nullable=False, 28 | server_default=sa.text("3"), 29 | default=3, 30 | ), 31 | ) 32 | 33 | 34 | # ### end Alembic commands ### 35 | 36 | 37 | def downgrade(): 38 | # ### commands auto generated by Alembic - please adjust! ### 39 | op.drop_column("artifact", "status") 40 | 41 | 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /zeus/migrations/6e7a43dc7b0e_missing_indexes.py: -------------------------------------------------------------------------------- 1 | """missing_indexes 2 | 3 | Revision ID: 6e7a43dc7b0e 4 | Revises: 1cb2c54f3831 5 | Create Date: 2018-03-31 15:41:16.048239 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "6e7a43dc7b0e" 14 | down_revision = "1cb2c54f3831" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_index( 22 | op.f("ix_build_date_created"), "build", ["date_created"], unique=False 23 | ) 24 | op.create_index( 25 | op.f("ix_repository_public"), "repository", ["public"], unique=False 26 | ) 27 | 28 | 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_index(op.f("ix_repository_public"), table_name="repository") 35 | op.drop_index(op.f("ix_build_date_created"), table_name="build") 36 | 37 | 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /zeus/migrations/70383b887d4a_cr_multi_author.py: -------------------------------------------------------------------------------- 1 | """cr_multi_author 2 | 3 | Revision ID: 70383b887d4a 4 | Revises: 8ee8825cd590 5 | Create Date: 2020-03-04 15:21:40.180785 6 | 7 | """ 8 | import zeus 9 | from alembic import op 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "70383b887d4a" 15 | down_revision = "8ee8825cd590" 16 | branch_labels = () 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.create_table( 22 | "change_request_author", 23 | sa.Column("change_request_id", zeus.db.types.guid.GUID(), nullable=False), 24 | sa.Column("author_id", zeus.db.types.guid.GUID(), nullable=False), 25 | sa.ForeignKeyConstraint(["author_id"], ["author.id"], ondelete="CASCADE"), 26 | sa.ForeignKeyConstraint( 27 | ["change_request_id"], ["change_request.id"], ondelete="CASCADE" 28 | ), 29 | sa.PrimaryKeyConstraint("change_request_id", "author_id"), 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_table("change_request_author") 35 | -------------------------------------------------------------------------------- /zeus/migrations/8bff5351578a_urls.py: -------------------------------------------------------------------------------- 1 | """urls 2 | 3 | Revision ID: 8bff5351578a 4 | Revises: 1fd74fb6ef0a 5 | Create Date: 2017-07-25 11:27:45.436698 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "8bff5351578a" 13 | down_revision = "1fd74fb6ef0a" 14 | branch_labels = () 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column("build", sa.Column("url", sa.String(), nullable=True)) 21 | op.add_column("job", sa.Column("url", sa.String(), nullable=True)) 22 | 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_column("job", "url") 30 | op.drop_column("build", "url") 31 | 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /zeus/migrations/8e598bda7fda_public_repository.py: -------------------------------------------------------------------------------- 1 | """public_repository 2 | 3 | Revision ID: 8e598bda7fda 4 | Revises: 694bcef51b94 5 | Create Date: 2018-01-20 17:29:34.783263 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "8e598bda7fda" 14 | down_revision = "694bcef51b94" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "repository", 23 | sa.Column("public", sa.Boolean(), server_default="false", nullable=False), 24 | ) 25 | 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("repository", "public") 33 | 34 | 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /zeus/migrations/8ee8825cd590_backfill_failure_build.py: -------------------------------------------------------------------------------- 1 | """backfill_failure_build 2 | 3 | Revision ID: 8ee8825cd590 4 | Revises: 14f53101b654 5 | Create Date: 2020-03-04 15:12:19.835756 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "8ee8825cd590" 14 | down_revision = "14f53101b654" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | connection = op.get_bind() 21 | connection.execute( 22 | """ 23 | update failurereason 24 | set build_id = (select build_id from job where job.id = failurereason.job_id) 25 | where build_id is null; 26 | """ 27 | ) 28 | 29 | 30 | def downgrade(): 31 | pass 32 | -------------------------------------------------------------------------------- /zeus/migrations/9d374079e8fc_hook_data.py: -------------------------------------------------------------------------------- 1 | """hook_data 2 | 3 | Revision ID: 9d374079e8fc 4 | Revises: 83dc0a466da2 5 | Create Date: 2017-11-08 15:27:00.872106 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import zeus 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "9d374079e8fc" 15 | down_revision = "83dc0a466da2" 16 | branch_labels = () 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column( 23 | "hook", sa.Column("data", zeus.db.types.json.JSONEncodedDict(), nullable=True) 24 | ) 25 | 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("hook", "data") 33 | 34 | 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /zeus/migrations/9dbc97018a55_migrate_github.py: -------------------------------------------------------------------------------- 1 | """migrate_github 2 | 3 | Revision ID: 9dbc97018a55 4 | Revises: f8013173ef21 5 | Create Date: 2017-08-27 14:20:59.219862 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "9dbc97018a55" 14 | down_revision = "f8013173ef21" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | conn = op.get_bind() 21 | conn.execute("update repository set provider = 'gh' where provider = 'github'") 22 | 23 | 24 | def downgrade(): 25 | conn = op.get_bind() 26 | conn.execute("update repository set provider = 'github' where provider = 'gh'") 27 | -------------------------------------------------------------------------------- /zeus/migrations/a456f2c3a68d_build_labels.py: -------------------------------------------------------------------------------- 1 | """build_labels 2 | 3 | Revision ID: a456f2c3a68d 4 | Revises: 240364a078d9 5 | Create Date: 2017-07-14 12:19:14.873485 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "a456f2c3a68d" 13 | down_revision = "240364a078d9" 14 | branch_labels = () 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.add_column("build", sa.Column("label", sa.String(), nullable=False)) 21 | op.add_column("job", sa.Column("label", sa.String(), nullable=True)) 22 | 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_column("job", "label") 30 | op.drop_column("build", "label") 31 | 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /zeus/migrations/a8bd86b031b4_add_testcase_date_created.py: -------------------------------------------------------------------------------- 1 | """add_testcase_date_created 2 | 3 | Revision ID: a8bd86b031b4 4 | Revises: 9096cdb5c97e 5 | Create Date: 2020-05-22 10:50:17.822843 6 | 7 | """ 8 | import zeus 9 | from alembic import op 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "a8bd86b031b4" 15 | down_revision = "9096cdb5c97e" 16 | branch_labels = () 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column( 23 | "testcase", 24 | sa.Column( 25 | "date_created", 26 | sa.TIMESTAMP(timezone=True), 27 | server_default=sa.text("now()"), 28 | nullable=False, 29 | ), 30 | ) 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_column("testcase", "date_created") 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /zeus/migrations/af3f4bdc27d1_cover_test_history.py: -------------------------------------------------------------------------------- 1 | """cover_test_history 2 | 3 | Revision ID: af3f4bdc27d1 4 | Revises: 33da83c61635 5 | Create Date: 2019-01-23 12:53:55.396720 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "af3f4bdc27d1" 14 | down_revision = "33da83c61635" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_index( 22 | "idx_testcase_repo_hash", 23 | "testcase", 24 | ["repository_id", "hash", "result"], 25 | unique=False, 26 | ) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_index("idx_testcase_repo_hash", table_name="testcase") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /zeus/migrations/bfe3af4f7eae_job_date_updated.py: -------------------------------------------------------------------------------- 1 | """job_date_updated 2 | 3 | Revision ID: bfe3af4f7eae 4 | Revises: fe3baeb0605e 5 | Create Date: 2017-11-15 15:27:07.022182 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "bfe3af4f7eae" 14 | down_revision = "fe3baeb0605e" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "job", sa.Column("date_updated", sa.TIMESTAMP(timezone=True), nullable=True) 23 | ) 24 | 25 | 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_column("job", "date_updated") 32 | 33 | 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /zeus/migrations/c257bd5a6236_email_verified.py: -------------------------------------------------------------------------------- 1 | """email_verified 2 | 3 | Revision ID: c257bd5a6236 4 | Revises: f810bb64d19b 5 | Create Date: 2017-10-07 14:16:25.952757 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "c257bd5a6236" 14 | down_revision = "f810bb64d19b" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column("email", "verified", existing_type=sa.BOOLEAN(), nullable=False) 22 | 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.alter_column("email", "verified", existing_type=sa.BOOLEAN(), nullable=True) 30 | 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /zeus/migrations/cd1324dcb6ba_remove_unused_index.py: -------------------------------------------------------------------------------- 1 | """remove_unused_index 2 | 3 | Revision ID: cd1324dcb6ba 4 | Revises: de49ae79ce38 5 | Create Date: 2019-01-23 12:44:47.439101 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "cd1324dcb6ba" 14 | down_revision = "de49ae79ce38" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_index("ix_build_author_id", table_name="build") 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.create_index("ix_build_author_id", "build", ["author_id"], unique=False) 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /zeus/migrations/de49ae79ce38_bundle_job_id_indexes.py: -------------------------------------------------------------------------------- 1 | """bundle_job_id_indexes 2 | 3 | Revision ID: de49ae79ce38 4 | Revises: 404ac069de83 5 | Create Date: 2019-01-22 12:58:06.135756 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "de49ae79ce38" 14 | down_revision = "404ac069de83" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_index(op.f("ix_bundle_job_id"), "bundle", ["job_id"], unique=False) 22 | op.create_index( 23 | op.f("ix_bundle_asset_job_id"), "bundle_asset", ["job_id"], unique=False 24 | ) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_index(op.f("ix_bundle_asset_job_id"), table_name="bundle_asset") 31 | op.drop_index(op.f("ix_bundle_job_id"), table_name="bundle") 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /zeus/migrations/e2f926a2b0fe_add_user_date_active.py: -------------------------------------------------------------------------------- 1 | """add_user_date_active 2 | 3 | Revision ID: e2f926a2b0fe 4 | Revises: e688aaea28d2 5 | Create Date: 2018-10-01 16:25:03.031284 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e2f926a2b0fe" 14 | down_revision = "e688aaea28d2" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "user", 22 | sa.Column( 23 | "date_active", 24 | sa.TIMESTAMP(timezone=True), 25 | server_default=sa.text("now()"), 26 | nullable=False, 27 | ), 28 | ) 29 | op.create_index(op.f("ix_user_date_active"), "user", ["date_active"], unique=False) 30 | connection = op.get_bind() 31 | connection.execute( 32 | """ 33 | update "user" set date_active = date_created 34 | """ 35 | ) 36 | 37 | 38 | def downgrade(): 39 | op.drop_index(op.f("ix_user_date_active"), table_name="user") 40 | op.drop_column("user", "date_active") 41 | -------------------------------------------------------------------------------- /zeus/migrations/e373a7bffa18_unique_build_failures.py: -------------------------------------------------------------------------------- 1 | """unique_build_failures 2 | 3 | Revision ID: e373a7bffa18 4 | Revises: 54bbb66a65a6 5 | Create Date: 2020-03-13 09:25:38.492704 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e373a7bffa18" 14 | down_revision = "54bbb66a65a6" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # first we clean up duplicate rows 21 | connection = op.get_bind() 22 | connection.execute( 23 | """ 24 | DELETE FROM failurereason a 25 | USING failurereason b 26 | WHERE a.id > b.id 27 | AND a.reason = b.reason 28 | AND a.build_id = b.build_id 29 | """ 30 | ) 31 | 32 | op.create_index( 33 | "unq_failurereason_buildonly", 34 | "failurereason", 35 | ["build_id", "reason"], 36 | unique=True, 37 | postgresql_where=sa.text("job_id IS NULL"), 38 | ) 39 | 40 | 41 | def downgrade(): 42 | op.drop_index("unq_failurereason_buildonly", table_name="failurereason") 43 | -------------------------------------------------------------------------------- /zeus/migrations/e688aaea28d2_backfill_build_author.py: -------------------------------------------------------------------------------- 1 | """backfill_build_author 2 | 3 | Revision ID: e688aaea28d2 4 | Revises: 523842e356fa 5 | Create Date: 2018-10-01 13:20:10.394355 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e688aaea28d2" 14 | down_revision = "523842e356fa" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | connection = op.get_bind() 21 | connection.execute( 22 | """ 23 | update build 24 | set author_id = (select author_id from source where build.source_id = source.id) 25 | where author_id is null; 26 | """ 27 | ) 28 | 29 | 30 | def downgrade(): 31 | pass 32 | -------------------------------------------------------------------------------- /zeus/migrations/f78a2be4ddf9_rename_hook_data.py: -------------------------------------------------------------------------------- 1 | """rename_hook_data 2 | 3 | Revision ID: f78a2be4ddf9 4 | Revises: d84e557ec8f6 5 | Create Date: 2018-01-12 09:04:17.878406 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "f78a2be4ddf9" 14 | down_revision = "d84e557ec8f6" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column("hook", "data", new_column_name="config") 21 | 22 | 23 | def downgrade(): 24 | op.alter_column("hook", "config", new_column_name="data") 25 | -------------------------------------------------------------------------------- /zeus/migrations/f8013173ef21_prefix_provider.py: -------------------------------------------------------------------------------- 1 | """prefix_provider 2 | 3 | Revision ID: f8013173ef21 4 | Revises: 9227d42a8935 5 | Create Date: 2017-08-27 14:12:28.917330 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "f8013173ef21" 14 | down_revision = "9227d42a8935" 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_constraint("unq_repo_name", "repository", type_="unique") 22 | op.create_unique_constraint( 23 | "unq_repo_name", "repository", ["provider", "owner_name", "name"] 24 | ) 25 | 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_constraint("unq_repo_name", "repository", type_="unique") 33 | op.create_unique_constraint("unq_repo_name", "repository", ["owner_name", "name"]) 34 | 35 | 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /zeus/migrations/fe3baeb0605e_merge_heads.py: -------------------------------------------------------------------------------- 1 | """merge heads 2 | 3 | Revision ID: fe3baeb0605e 4 | Revises: b3eb342cfd7e, 9d374079e8fc 5 | Create Date: 2017-11-15 15:26:40.975005 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "fe3baeb0605e" 14 | down_revision = ("b3eb342cfd7e", "9d374079e8fc") 15 | branch_labels = () 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | pass 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /zeus/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /zeus/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .api_token_repository_access import * # NOQA 2 | from .api_token import * # NOQA 3 | from .artifact import * # NOQA 4 | from .author import * # NOQA 5 | from .build import * # NOQA 6 | from .bundlestat import * # NOQA 7 | from .change_request import * # NOQA 8 | from .email import * # NOQA 9 | from .failurereason import * # NOQA 10 | from .filecoverage import * # NOQA 11 | from .hook import * # NOQA 12 | from .identity import * # NOQA 13 | from .itemoption import * # NOQA 14 | from .itemsequence import * # NOQA 15 | from .itemstat import * # NOQA 16 | from .job import * # NOQA 17 | from .pending_artifact import * # NOQA 18 | from .repository_access import * # NOQA 19 | from .repository_api_token import * # NOQA 20 | from .repository import * # NOQA 21 | from .revision import * # NOQA 22 | from .styleviolation import * # NOQA 23 | from .testcase import * # NOQA 24 | from .testcase_meta import * # NOQA 25 | from .testcase_rollup import * # NOQA 26 | from .user_api_token import * # NOQA 27 | from .user import * # NOQA 28 | -------------------------------------------------------------------------------- /zeus/models/api_token_repository_access.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.constants import Permission 3 | from zeus.db.types import Enum, GUID 4 | from zeus.db.utils import model_repr 5 | 6 | 7 | class ApiTokenRepositoryAccess(db.Model): 8 | repository_id = db.Column( 9 | GUID, db.ForeignKey("repository.id", ondelete="CASCADE"), primary_key=True 10 | ) 11 | api_token_id = db.Column( 12 | GUID, db.ForeignKey("api_token.id", ondelete="CASCADE"), primary_key=True 13 | ) 14 | permission = db.Column(Enum(Permission), nullable=False, default=Permission.read) 15 | 16 | repository = db.relationship("Repository", innerjoin=True, uselist=False) 17 | api_token = db.relationship("ApiToken", innerjoin=True, uselist=False) 18 | 19 | __tablename__ = "api_token_repository_access" 20 | __repr__ = model_repr("repository_id", "api_token_id") 21 | -------------------------------------------------------------------------------- /zeus/models/author.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.mixins import RepositoryBoundMixin 3 | from zeus.db.types import GUID 4 | from zeus.db.utils import model_repr 5 | 6 | 7 | class Author(RepositoryBoundMixin, db.Model): 8 | """ 9 | The author of a source. Generally used for things like commit authors. 10 | 11 | This is different than User, which indicates a known authenticatable user. 12 | """ 13 | 14 | id = db.Column(GUID, primary_key=True, default=GUID.default_value) 15 | name = db.Column(db.String(128), nullable=False) 16 | email = db.Column(db.String(128), nullable=True) 17 | 18 | __tablename__ = "author" 19 | __table_args__ = ( 20 | db.UniqueConstraint("repository_id", "email", name="unq_author_email"), 21 | ) 22 | __repr__ = model_repr("repository_id", "name", "email") 23 | -------------------------------------------------------------------------------- /zeus/models/email.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.mixins import StandardAttributes 3 | from zeus.db.types import GUID 4 | from zeus.db.utils import model_repr 5 | 6 | 7 | class Email(StandardAttributes, db.Model): 8 | """ 9 | An email address associated with a user. 10 | """ 11 | 12 | user_id = db.Column( 13 | GUID, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False, index=True 14 | ) 15 | email = db.Column(db.String(128), nullable=False) 16 | verified = db.Column(db.Boolean, default=False, nullable=False) 17 | 18 | user = db.relationship("User") 19 | 20 | __tablename__ = "email" 21 | __table_args__ = (db.UniqueConstraint("user_id", "email", name="unq_user_email"),) 22 | __repr__ = model_repr("user_id", "email", "verified") 23 | -------------------------------------------------------------------------------- /zeus/models/identity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.dialects.postgresql import ARRAY 2 | 3 | from zeus.config import db 4 | from zeus.db.mixins import StandardAttributes 5 | from zeus.db.types import GUID, JSONEncodedDict 6 | from zeus.db.utils import model_repr 7 | 8 | 9 | class Identity(StandardAttributes, db.Model): 10 | """ 11 | Identities associated with a user. Primarily used for Single Sign-On. 12 | """ 13 | 14 | user_id = db.Column( 15 | GUID, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False, index=True 16 | ) 17 | external_id = db.Column(db.String(64), unique=True, nullable=False) 18 | provider = db.Column(db.String(32), nullable=False) 19 | config = db.Column(JSONEncodedDict, nullable=False) 20 | scopes = db.Column(ARRAY(db.String(64)), nullable=True) 21 | 22 | user = db.relationship("User") 23 | 24 | __tablename__ = "identity" 25 | __table_args__ = ( 26 | db.UniqueConstraint("user_id", "provider", name="unq_identity_user"), 27 | ) 28 | __repr__ = model_repr("user_id", "provider", "external_id") 29 | -------------------------------------------------------------------------------- /zeus/models/itemoption.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.types import GUID 3 | from zeus.db.utils import model_repr 4 | 5 | 6 | class ItemOption(db.Model): 7 | id = db.Column(GUID, primary_key=True, default=GUID.default_value) 8 | item_id = db.Column(GUID, nullable=False) 9 | name = db.Column(db.String(64), nullable=False) 10 | value = db.Column(db.Text, nullable=False) 11 | 12 | __tablename__ = "itemoption" 13 | __table_args__ = ( 14 | db.UniqueConstraint("item_id", "name", name="unq_itemoption_name"), 15 | ) 16 | __repr__ = model_repr("item_id", "name") 17 | -------------------------------------------------------------------------------- /zeus/models/itemsequence.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.types import GUID 3 | from zeus.db.utils import model_repr 4 | 5 | 6 | class ItemSequence(db.Model): 7 | parent_id = db.Column(GUID, nullable=False, primary_key=True) 8 | value = db.Column( 9 | db.Integer, default=0, server_default="0", nullable=False, primary_key=True 10 | ) 11 | 12 | __tablename__ = "itemsequence" 13 | __repr__ = model_repr("parent_id", "value") 14 | -------------------------------------------------------------------------------- /zeus/models/itemstat.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.types import GUID 3 | from zeus.db.utils import model_repr 4 | 5 | 6 | class ItemStat(db.Model): 7 | id = db.Column(GUID, primary_key=True, default=GUID.default_value) 8 | item_id = db.Column(GUID, nullable=False) 9 | name = db.Column(db.String(64), nullable=False) 10 | value = db.Column(db.Integer, nullable=False) 11 | 12 | __tablename__ = "itemstat" 13 | __table_args__ = (db.UniqueConstraint("item_id", "name", name="unq_itemstat_name"),) 14 | __repr__ = model_repr("item_id", "name") 15 | -------------------------------------------------------------------------------- /zeus/models/repository_access.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.constants import Permission 3 | from zeus.db.types import Enum, GUID 4 | from zeus.db.utils import model_repr 5 | 6 | 7 | class RepositoryAccess(db.Model): 8 | repository_id = db.Column( 9 | GUID, db.ForeignKey("repository.id", ondelete="CASCADE"), primary_key=True 10 | ) 11 | user_id = db.Column( 12 | GUID, db.ForeignKey("user.id", ondelete="CASCADE"), primary_key=True 13 | ) 14 | permission = db.Column( 15 | Enum(Permission), nullable=False, default=Permission.read, server_default="1" 16 | ) 17 | 18 | repository = db.relationship("Repository", innerjoin=True, uselist=False) 19 | user = db.relationship("User", innerjoin=True, uselist=False) 20 | 21 | __tablename__ = "repository_access" 22 | __repr__ = model_repr("repository_id", "user_id") 23 | -------------------------------------------------------------------------------- /zeus/models/repository_api_token.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.mixins import ApiTokenMixin, RepositoryMixin, StandardAttributes 3 | from zeus.db.utils import model_repr 4 | 5 | 6 | class RepositoryApiToken(StandardAttributes, RepositoryMixin, ApiTokenMixin, db.Model): 7 | """ 8 | An API token associated to a repository. 9 | """ 10 | 11 | __tablename__ = "repository_api_token" 12 | __repr__ = model_repr("repository_id", "key") 13 | 14 | def get_token_key(self): 15 | return "r" 16 | -------------------------------------------------------------------------------- /zeus/models/styleviolation.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.constants import Severity 3 | from zeus.db.mixins import RepositoryBoundMixin, StandardAttributes 4 | from zeus.db.types import Enum, GUID 5 | from zeus.db.utils import model_repr 6 | 7 | 8 | class StyleViolation(RepositoryBoundMixin, StandardAttributes, db.Model): 9 | """ 10 | A single style violation. 11 | """ 12 | 13 | job_id = db.Column( 14 | GUID, db.ForeignKey("job.id", ondelete="CASCADE"), nullable=False 15 | ) 16 | filename = db.Column(db.Text, nullable=False) 17 | severity = db.Column(Enum(Severity), default=Severity.error, nullable=False) 18 | message = db.Column(db.Text, nullable=False) 19 | lineno = db.Column(db.Integer, nullable=True) 20 | colno = db.Column(db.Integer, nullable=True) 21 | source = db.Column(db.Text, nullable=True) 22 | 23 | job = db.relationship("Job") 24 | 25 | __tablename__ = "styleviolation" 26 | __repr__ = model_repr("repository_id", "job_id", "filename", "message") 27 | -------------------------------------------------------------------------------- /zeus/models/testcase_meta.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.mixins import RepositoryBoundMixin 3 | from zeus.db.types import GUID 4 | from zeus.db.utils import model_repr 5 | 6 | 7 | class TestCaseMeta(RepositoryBoundMixin, db.Model): 8 | """ 9 | A materialized view of a testcase's historical properties. 10 | """ 11 | 12 | id = db.Column(GUID, nullable=False, primary_key=True, default=GUID.default_value) 13 | hash = db.Column(db.String(40), nullable=False) 14 | name = db.Column(db.Text, nullable=False) 15 | first_build_id = db.Column( 16 | GUID, db.ForeignKey("build.id", ondelete="CASCADE"), nullable=False 17 | ) 18 | 19 | first_build = db.relationship("Build") 20 | 21 | __tablename__ = "testcase_meta" 22 | __table_args__ = ( 23 | db.UniqueConstraint("repository_id", "hash", name="unq_testcase_meta_hash"), 24 | ) 25 | __repr__ = model_repr("repository_id", "name") 26 | -------------------------------------------------------------------------------- /zeus/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import func 2 | 3 | from zeus.config import db 4 | from zeus.db.mixins import StandardAttributes 5 | from zeus.db.utils import model_repr 6 | from zeus.utils import timezone 7 | 8 | 9 | class User(StandardAttributes, db.Model): 10 | """ 11 | Actors within Zeus. 12 | """ 13 | 14 | email = db.Column(db.String(128), unique=True, nullable=False) 15 | date_active = db.Column( 16 | db.TIMESTAMP(timezone=True), 17 | nullable=False, 18 | default=timezone.now, 19 | server_default=func.now(), 20 | index=True, 21 | ) 22 | 23 | options = db.relationship( 24 | "ItemOption", 25 | foreign_keys="[ItemOption.item_id]", 26 | primaryjoin="ItemOption.item_id == User.id", 27 | viewonly=True, 28 | uselist=True, 29 | ) 30 | 31 | __tablename__ = "user" 32 | __repr__ = model_repr("email") 33 | -------------------------------------------------------------------------------- /zeus/models/user_api_token.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.db.mixins import ApiTokenMixin, StandardAttributes 3 | from zeus.db.types import GUID 4 | from zeus.db.utils import model_repr 5 | 6 | 7 | class UserApiToken(StandardAttributes, db.Model, ApiTokenMixin): 8 | """ 9 | An API token associated to users. 10 | """ 11 | 12 | user_id = db.Column( 13 | GUID, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False, unique=True 14 | ) 15 | 16 | user = db.relationship( 17 | "User", backref=db.backref("tokens", uselist=False), innerjoin=True 18 | ) 19 | 20 | __tablename__ = "user_api_token" 21 | __repr__ = model_repr("user_id", "key") 22 | 23 | def get_token_key(self): 24 | return "u" 25 | -------------------------------------------------------------------------------- /zeus/notifications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/notifications/__init__.py -------------------------------------------------------------------------------- /zeus/providers/__init__.py: -------------------------------------------------------------------------------- 1 | from .custom import CustomProvider 2 | from .travis import TravisProvider 3 | 4 | ALIASES = {"travis-ci": "travis"} 5 | 6 | PROVIDERS = {"travis": TravisProvider, "custom": CustomProvider} 7 | 8 | VALID_PROVIDER_NAMES = tuple(set(PROVIDERS.keys()).union(ALIASES.keys())) 9 | 10 | 11 | class InvalidProvider(Exception): 12 | pass 13 | 14 | 15 | def get_provider(provider_name): 16 | try: 17 | return PROVIDERS[ALIASES.get(provider_name, provider_name)]() 18 | except KeyError: 19 | raise InvalidProvider(provider_name) 20 | -------------------------------------------------------------------------------- /zeus/providers/custom.py: -------------------------------------------------------------------------------- 1 | from zeus.providers.base import Provider 2 | 3 | 4 | class CustomProvider(Provider): 5 | def get_config(self): 6 | return {"properties": {"name": {"type": "string"}}, "required": ["name"]} 7 | 8 | def get_name(self, config): 9 | return config.get("name", "custom") 10 | -------------------------------------------------------------------------------- /zeus/providers/travis/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # NOQA 2 | -------------------------------------------------------------------------------- /zeus/providers/travis/base.py: -------------------------------------------------------------------------------- 1 | from zeus.providers.base import Provider 2 | 3 | 4 | class TravisProvider(Provider): 5 | def get_config(self): 6 | return { 7 | "properties": { 8 | "domain": { 9 | "type": "string", 10 | "enum": ["api.travis-ci.com", "api.travis-ci.org"], 11 | "default": "api.travis-ci.org", 12 | } 13 | }, 14 | "required": ["domain"], 15 | } 16 | 17 | def get_name(self, config): 18 | return config.get("domain", "api.travis-ci.org") 19 | -------------------------------------------------------------------------------- /zeus/pubsub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/pubsub/__init__.py -------------------------------------------------------------------------------- /zeus/pubsub/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from uuid import uuid4 4 | 5 | from zeus.config import redis 6 | 7 | 8 | def publish(channel, event, data): 9 | redis.publish( 10 | channel, json.dumps({"id": uuid4().hex, "event": event, "data": data}) 11 | ) 12 | -------------------------------------------------------------------------------- /zeus/storage/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # NOQA 2 | from .gcs import * # NOQA 3 | from .mock import * # NOQA 4 | -------------------------------------------------------------------------------- /zeus/storage/base.py: -------------------------------------------------------------------------------- 1 | class FileStorage(object): 2 | def __init__(self, path=""): 3 | self.path = path 4 | 5 | def delete(self, filename): 6 | raise NotImplementedError 7 | 8 | def save(self, filename, fp): 9 | raise NotImplementedError 10 | 11 | def url_for(self, filename, expire=300): 12 | raise NotImplementedError 13 | 14 | def get_file(self, filename): 15 | raise NotImplementedError 16 | -------------------------------------------------------------------------------- /zeus/storage/mock.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Dict 3 | 4 | from .base import FileStorage 5 | 6 | _cache: Dict[str, bytes] = {} 7 | 8 | 9 | class FileStorageCache(FileStorage): 10 | global _cache 11 | 12 | def delete(self, filename: str): 13 | _cache.pop(filename, None) 14 | 15 | def save(self, filename: str, fp): 16 | _cache[filename] = fp.read() 17 | 18 | def url_for(self, filename: str, expire: int = 300) -> str: 19 | return "https://example.com/artifacts/{}".format(filename) 20 | 21 | def get_file(self, filename: str) -> BytesIO: 22 | return BytesIO(_cache[filename]) 23 | 24 | @staticmethod 25 | def clear(): 26 | _cache.clear() 27 | -------------------------------------------------------------------------------- /zeus/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .aggregate_job_stats import * # NOQA 2 | from .cleanup_artifacts import * # NOQA 3 | from .cleanup_builds import * # NOQA 4 | from .cleanup_pending_artifacts import * # NOQA 5 | from .deactivate_repo import * # NOQA 6 | from .delete_repo import * # NOQA 7 | from .process_artifact import * # NOQA 8 | from .process_pending_artifact import * # NOQA 9 | from .process_travis_webhook import * # NOQA 10 | from .resolve_ref import * # NOQA 11 | from .send_build_notifications import * # NOQA 12 | from .sync_github_access import * # NOQA 13 | from .testcase_rollups import * # NOQA 14 | -------------------------------------------------------------------------------- /zeus/testutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/testutils/__init__.py -------------------------------------------------------------------------------- /zeus/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/utils/__init__.py -------------------------------------------------------------------------------- /zeus/utils/artifacts.py: -------------------------------------------------------------------------------- 1 | from zeus.config import db 2 | from zeus.constants import Status 3 | from zeus.models import Artifact, Job, PendingArtifact 4 | 5 | 6 | def has_unprocessed_artifacts(job: Job) -> bool: 7 | if db.session.query( 8 | Artifact.query.filter( 9 | Artifact.status != Status.finished, Artifact.job_id == job.id 10 | ).exists() 11 | ).scalar(): 12 | return True 13 | if ( 14 | job.external_id 15 | and db.session.query( 16 | PendingArtifact.query.filter( 17 | PendingArtifact.repository_id == job.repository_id, 18 | PendingArtifact.provider == job.provider, 19 | PendingArtifact.external_build_id == job.build.external_id, 20 | PendingArtifact.external_job_id == job.external_id, 21 | ).exists() 22 | ).scalar() 23 | ): 24 | return True 25 | return False 26 | -------------------------------------------------------------------------------- /zeus/utils/asyncio.py: -------------------------------------------------------------------------------- 1 | # https://github.com/pallets/click/issues/85#issuecomment-503464628 2 | import asyncio 3 | import asyncpg 4 | 5 | from flask import current_app 6 | from functools import wraps 7 | 8 | 9 | def coroutine(f): 10 | @wraps(f) 11 | def wrapper(*args, **kwargs): 12 | return asyncio.run(f(*args, **kwargs)) 13 | 14 | return wrapper 15 | 16 | 17 | async def create_db_pool(current_app=current_app): 18 | return await asyncpg.create_pool( 19 | host=current_app.config["DB_HOST"], 20 | port=current_app.config["DB_PORT"], 21 | user=current_app.config["DB_USER"], 22 | password=current_app.config["DB_PASSWORD"], 23 | database=current_app.config["DB_NAME"], 24 | # we want to rely on pgbouncer 25 | statement_cache_size=0, 26 | ) 27 | -------------------------------------------------------------------------------- /zeus/utils/email.py: -------------------------------------------------------------------------------- 1 | import toronado 2 | import lxml 3 | 4 | 5 | def inline_css(value): 6 | tree = lxml.html.document_fromstring(value) 7 | toronado.inline(tree) 8 | # CSS media query support is inconistent when the DOCTYPE declaration is 9 | # missing, so we force it to HTML5 here. 10 | return lxml.html.tostring(tree, doctype="").decode("utf-8") 11 | -------------------------------------------------------------------------------- /zeus/utils/functional.py: -------------------------------------------------------------------------------- 1 | class memoize(object): 2 | """ 3 | Memoize the result of a property call. 4 | 5 | >>> class A(object): 6 | >>> @memoize 7 | >>> def func(self): 8 | >>> return 'foo' 9 | """ 10 | 11 | def __init__(self, func): 12 | self.__name__ = func.__name__ 13 | self.__module__ = func.__module__ 14 | self.__doc__ = func.__doc__ 15 | self.func = func 16 | 17 | def __get__(self, obj, type=None): 18 | if obj is None: 19 | return self 20 | 21 | d, n = vars(obj), self.__name__ 22 | if n not in d: 23 | value = self.func(obj) 24 | d[n] = value 25 | return value 26 | -------------------------------------------------------------------------------- /zeus/utils/http.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | 4 | def absolute_url(path: str) -> str: 5 | return "{proto}://{domain}/{path}".format( 6 | proto="https" if current_app.config["SSL"] else "http", 7 | domain=current_app.config["DOMAIN"], 8 | path=path.lstrip("/"), 9 | ) 10 | -------------------------------------------------------------------------------- /zeus/utils/imports.py: -------------------------------------------------------------------------------- 1 | def import_string(path: str): 2 | """ 3 | Path must be module.path.ClassName 4 | 5 | >>> cls = import_string('sentry.models.Group') 6 | """ 7 | if "." not in path: 8 | return __import__(path) 9 | 10 | module_name, class_name = path.rsplit(".", 1) 11 | 12 | module = __import__(module_name, {}, {}, [class_name]) 13 | try: 14 | return getattr(module, class_name) 15 | 16 | except AttributeError as exc: 17 | raise ImportError from exc 18 | -------------------------------------------------------------------------------- /zeus/utils/ssh.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from cryptography.hazmat.primitives import serialization 3 | from cryptography.hazmat.primitives.asymmetric import rsa 4 | from cryptography.hazmat.backends import default_backend 5 | 6 | KeyPair = namedtuple("KeyPair", ["private_key", "public_key"]) 7 | 8 | 9 | def generate_key(): 10 | # generate private/public key pair 11 | key = rsa.generate_private_key( 12 | backend=default_backend(), public_exponent=65537, key_size=2048 13 | ) 14 | 15 | # get public key in OpenSSH format 16 | public_key = key.public_key().public_bytes( 17 | serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH 18 | ) 19 | 20 | # get private key in PEM container format 21 | pem = key.private_bytes( 22 | encoding=serialization.Encoding.PEM, 23 | format=serialization.PrivateFormat.TraditionalOpenSSL, 24 | encryption_algorithm=serialization.NoEncryption(), 25 | ) 26 | 27 | return KeyPair(pem.decode("utf-8"), public_key.decode("utf-8")) 28 | -------------------------------------------------------------------------------- /zeus/utils/text.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from typing import List 4 | from unidecode import unidecode 5 | 6 | _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') 7 | 8 | 9 | def slugify(text: str, delim: str = "-") -> str: 10 | """ 11 | Generates an ASCII-only slug. 12 | """ 13 | result: List[str] = [] 14 | for word in _punct_re.split(text.lower()): 15 | result.extend(unidecode(word).split()) 16 | return str(delim.join(result)) 17 | -------------------------------------------------------------------------------- /zeus/utils/timezone.py: -------------------------------------------------------------------------------- 1 | # this module implements an interface similar to django.utils.timezone 2 | from datetime import datetime, timezone 3 | 4 | utc = timezone.utc 5 | 6 | 7 | def now(tzinfo=timezone.utc): 8 | return datetime.now(tzinfo) 9 | 10 | 11 | def fromtimestamp(ts, tzinfo=timezone.utc): 12 | return datetime.utcfromtimestamp(ts).replace(tzinfo=tzinfo) 13 | -------------------------------------------------------------------------------- /zeus/vcs/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import vcs_client # NOQA 2 | -------------------------------------------------------------------------------- /zeus/vcs/asserts.py: -------------------------------------------------------------------------------- 1 | def assert_revision(revision, author=None, message=None): 2 | """Asserts values of the given fields in the provided revision. 3 | 4 | :param revision: The revision to validate 5 | :param author: that must be present in the ``revision`` 6 | :param message: message substring that must be present in ``revision`` 7 | """ 8 | if author: 9 | assert author == revision.author 10 | if message: 11 | assert message in revision.message 12 | -------------------------------------------------------------------------------- /zeus/vcs/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/vcs/backends/__init__.py -------------------------------------------------------------------------------- /zeus/vcs/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/zeus/6d4a490c19ebe406b551641a022ca08f26c21fcb/zeus/vcs/providers/__init__.py -------------------------------------------------------------------------------- /zeus/vcs/providers/base.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from zeus.models import Repository, User 4 | from zeus.utils.ssh import KeyPair 5 | 6 | 7 | # this API is very much a work in progress, and hasn't yet had a lot of thought put 8 | # into it 9 | 10 | 11 | class RepositoryProvider(object): 12 | def __init__(self, cache=True): 13 | self.cache = cache 14 | 15 | def get_owners(self, user: User) -> List[dict]: 16 | raise NotImplementedError 17 | 18 | def get_repos_for_owner(self, user: User, owner_name: str) -> List[dict]: 19 | raise NotImplementedError 20 | 21 | def get_repo(self, user: User, owner_name: str, repo_name: str) -> dict: 22 | raise NotImplementedError 23 | 24 | def add_key(self, user: User, owner_name: str, repo_name: str, key: KeyPair): 25 | raise NotImplementedError 26 | 27 | def has_access(self, repository: Repository, user: User) -> bool: 28 | raise NotImplementedError 29 | -------------------------------------------------------------------------------- /zeus/web/debug/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | from .notification import debug_notification 4 | 5 | app = Blueprint("debug", __name__) 6 | app.add_url_rule("/mail/notification", view_func=debug_notification) 7 | -------------------------------------------------------------------------------- /zeus/web/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | from .build import * # NOQA 2 | from .change_request import * # NOQA 3 | from .job_artifacts import * # NOQA 4 | from .job import * # NOQA 5 | -------------------------------------------------------------------------------- /zeus/web/hooks/build.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from zeus.api.utils.upserts import upsert_build 4 | 5 | from .base import BaseHook 6 | 7 | 8 | class BuildHook(BaseHook): 9 | def post(self, hook, build_xid): 10 | return upsert_build( 11 | hook=hook, external_id=build_xid, data=request.get_json() or {} 12 | ) 13 | -------------------------------------------------------------------------------- /zeus/web/hooks/change_request.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from zeus.api.utils.upserts import upsert_change_request 4 | 5 | from .base import BaseHook 6 | 7 | 8 | class ChangeRequestHook(BaseHook): 9 | def post(self, hook, cr_xid): 10 | return upsert_change_request( 11 | provider=hook.provider, external_id=cr_xid, data=request.get_json() or {} 12 | ) 13 | -------------------------------------------------------------------------------- /zeus/web/hooks/job.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from zeus.models import Build 4 | from zeus.api.utils.upserts import upsert_job 5 | 6 | from .base import BaseHook 7 | 8 | 9 | class JobHook(BaseHook): 10 | def post(self, hook, build_xid, job_xid): 11 | provider_name = hook.get_provider().get_name(hook.config) 12 | build = Build.query.filter( 13 | Build.provider == provider_name, Build.external_id == build_xid 14 | ).first() 15 | if not build: 16 | return self.respond("", 404) 17 | 18 | return upsert_job( 19 | build=build, hook=hook, external_id=job_xid, data=request.get_json() or {} 20 | ) 21 | -------------------------------------------------------------------------------- /zeus/web/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_github import * # NOQA 2 | from .index import * # NOQA 3 | -------------------------------------------------------------------------------- /zeus/web/views/index.py: -------------------------------------------------------------------------------- 1 | from flask import current_app, render_template 2 | 3 | 4 | def index(path=None): 5 | return render_template( 6 | "index.html", 7 | **{ 8 | "PUBSUB_ENDPOINT": current_app.config.get("PUBSUB_ENDPOINT") or "", 9 | "SENTRY_DSN_FRONTEND": current_app.config.get("SENTRY_DSN_FRONTEND") or "", 10 | "SENTRY_ENVIRONMENT": current_app.config.get("SENTRY_ENVIRONMENT") or "", 11 | "SENTRY_RELEASE": current_app.config.get("SENTRY_RELEASE") or "", 12 | } 13 | ) 14 | --------------------------------------------------------------------------------