├── .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 |
26 | {text}
27 |
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 |
19 |
20 | Overview
21 |
22 | Commits
23 | Change Requests
24 | Builds
25 | Reports
26 | {!!repo.permissions.admin && (
27 | Settings
28 | )}
29 |
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 |
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 |
8 | 2 days ago
9 |
10 | `;
11 |
12 | exports[`TimeSince renders default 1`] = `
13 |
17 | 2 days ago
18 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------