├── .circleci ├── config.yml └── lock.sh ├── .dockerignore ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── build.sh ├── cmd ├── buildrunner │ └── main.go ├── containers_orchestrator │ └── main.go ├── ensuredeps │ └── main.go ├── genservices │ └── main.go ├── getrepoinfo │ └── main.go ├── gocodescore │ └── main.go ├── goenvbuild │ └── main.go ├── golangci-api │ └── main.go └── golangci-worker │ └── main.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── api │ ├── apierrors │ │ └── errors.go │ ├── endpointutil │ │ ├── context.go │ │ └── handler.go │ ├── events │ │ ├── amplitude.go │ │ ├── mixpanel.go │ │ └── tracker.go │ ├── paymentproviders │ │ ├── factory.go │ │ ├── implementations │ │ │ ├── paddle │ │ │ │ ├── const.go │ │ │ │ ├── event_processor.go │ │ │ │ ├── events.go │ │ │ │ ├── form_util.go │ │ │ │ ├── provider.go │ │ │ │ └── request.go │ │ │ ├── securionpay.go │ │ │ └── stable.go │ │ └── paymentprovider │ │ │ ├── errors.go │ │ │ ├── event_processor.go │ │ │ ├── models.go │ │ │ └── provider.go │ ├── score │ │ └── calculator.go │ ├── session │ │ ├── factory.go │ │ ├── request_context.go │ │ ├── saver.go │ │ └── session.go │ ├── transportutil │ │ ├── context.go │ │ ├── decode.go │ │ ├── encode.go │ │ ├── errors.go │ │ ├── log.go │ │ └── session.go │ └── util │ │ └── random.go └── shared │ ├── apperrors │ ├── log.go │ ├── nop_tracker.go │ ├── rollbar_tracker.go │ ├── sentry_tracker.go │ ├── tracker.go │ └── tracker_builder.go │ ├── cache │ ├── cache.go │ └── redis.go │ ├── config │ ├── config.go │ └── env.go │ ├── db │ ├── gormdb │ │ ├── gormdb.go │ │ ├── logger.go │ │ ├── sql.go │ │ └── tx.go │ ├── migrations │ │ └── runner.go │ └── redis │ │ └── pool.go │ ├── fsutil │ └── wd.go │ ├── logutil │ ├── context.go │ ├── log.go │ └── stderr_log.go │ ├── providers │ ├── factory.go │ ├── implementations │ │ ├── github.go │ │ └── stable.go │ └── provider │ │ ├── errors.go │ │ ├── models.go │ │ └── provider.go │ └── queue │ ├── aws │ ├── consumer │ │ └── consumer.go │ └── sqs │ │ └── queue.go │ ├── consumers │ ├── consumer.go │ ├── errors.go │ ├── multiplexer.go │ └── reflect_consumer.go │ ├── message.go │ └── producers │ ├── base.go │ ├── multiplexer.go │ └── queue.go ├── migrations ├── 10_add_github_hook_id_to_github_repos.down.sql ├── 10_add_github_hook_id_to_github_repos.up.sql ├── 11_create_github_analyzes.down.sql ├── 11_create_github_analyzes.up.sql ├── 12_add_reported_issues_count_to_github_analyzes.down.sql ├── 12_add_reported_issues_count_to_github_analyzes.up.sql ├── 13_add_commit_sha_to_github_analyzes.down.sql ├── 13_add_commit_sha_to_github_analyzes.up.sql ├── 14_add_private_access_token_to_github_auths.down.sql ├── 14_add_private_access_token_to_github_auths.up.sql ├── 15_drop_commit_sha_and_repo_uniq_index_from_github_analyzes.down.sql ├── 15_drop_commit_sha_and_repo_uniq_index_from_github_analyzes.up.sql ├── 16_add_result_json_to_github_analyzes.down.sql ├── 16_add_result_json_to_github_analyzes.up.sql ├── 17_add_index_on_github_pull_request_number_to_github_analyzes.down.sql ├── 17_add_index_on_github_pull_request_number_to_github_analyzes.up.sql ├── 18_create_repo_analysis_statuses.down.sql ├── 18_create_repo_analysis_statuses.up.sql ├── 19_create_repo_analyzes.down.sql ├── 19_create_repo_analyzes.up.sql ├── 1_add_users.down.sql ├── 1_add_users.up.sql ├── 20_add_fields_to_repo_analysis_statuses.down.sql ├── 20_add_fields_to_repo_analysis_statuses.up.sql ├── 21_add_default_branch_to_repo_analysis_statuses.down.sql ├── 21_add_default_branch_to_repo_analysis_statuses.up.sql ├── 22_add_commit_sha_to_repo_analyzes.down.sql ├── 22_add_commit_sha_to_repo_analyzes.up.sql ├── 23_add_index_on_status_to_repo_analyzes.down.sql ├── 23_add_index_on_status_to_repo_analyzes.up.sql ├── 24_add_attempt_number_repo_analyzes.down.sql ├── 24_add_attempt_number_repo_analyzes.up.sql ├── 25_add_linters_version_to_repo_analyzes.down.sql ├── 25_add_linters_version_to_repo_analyzes.up.sql ├── 26_add_last_analyzed_linters_version_to_repo_analysis_statuses.down.sql ├── 26_add_last_analyzed_linters_version_to_repo_analysis_statuses.up.sql ├── 27_add_active_to_repo_analysis_statuses.down.sql ├── 27_add_active_to_repo_analysis_statuses.up.sql ├── 28_add_display_name_to_github_repos.down.sql ├── 28_add_display_name_to_github_repos.up.sql ├── 29_lowercase_names_in_github_repos.down.sql ├── 29_lowercase_names_in_github_repos.up.sql ├── 2_add_github_auths.down.sql ├── 2_add_github_auths.up.sql ├── 30_add_github_user_id_to_github_auths.down.sql ├── 30_add_github_user_id_to_github_auths.up.sql ├── 31_add_index_on_github_user_id_to_github_auths.down.sql ├── 31_add_index_on_github_user_id_to_github_auths.up.sql ├── 32_drop_uniq_login_index_from_github_auths.down.sql ├── 32_drop_uniq_login_index_from_github_auths.up.sql ├── 33_add_github_id_to_github_repos.down.sql ├── 33_add_github_id_to_github_repos.up.sql ├── 34_rename_github_repos_to_repos.down.sql ├── 34_rename_github_repos_to_repos.up.sql ├── 35_rename_github_analyzes_to_pull_request_analyzes.down.sql ├── 35_rename_github_analyzes_to_pull_request_analyzes.up.sql ├── 36_remove_github_from_some_columns_in_pull_request_analyzes.down.sql ├── 36_remove_github_from_some_columns_in_pull_request_analyzes.up.sql ├── 37_add_provider_to_repos.down.sql ├── 37_add_provider_to_repos.up.sql ├── 38_add_repo_id_to_repo_analysis_statuses.down.sql ├── 38_add_repo_id_to_repo_analysis_statuses.up.sql ├── 39_add_index_on_repo_analysis_status_id_to_repo_analyzes.down.sql ├── 39_add_index_on_repo_analysis_status_id_to_repo_analyzes.up.sql ├── 3_add_index_on_nickname_to_users.down.sql ├── 3_add_index_on_nickname_to_users.up.sql ├── 40_make_repo_id_not_null_in_repo_analysis_statuses.down.sql ├── 40_make_repo_id_not_null_in_repo_analysis_statuses.up.sql ├── 41_rename_github_auths_to_auths.down.sql ├── 41_rename_github_auths_to_auths.up.sql ├── 42_rename_github_to_provider_in_auths.down.sql ├── 42_rename_github_to_provider_in_auths.up.sql ├── 43_rename_github_to_provider_in_repos.down.sql ├── 43_rename_github_to_provider_in_repos.up.sql ├── 44_add_commit_state_to_repos.down.sql ├── 44_add_commit_state_to_repos.up.sql ├── 45_add_stargazers_count_to_repos.down.sql ├── 45_add_stargazers_count_to_repos.up.sql ├── 46_add_orgs.down.sql ├── 46_add_orgs.up.sql ├── 47_add_org_subs.down.sql ├── 47_add_org_subs.up.sql ├── 48_add_idempotency_key_to_org_subs.down.sql ├── 48_add_idempotency_key_to_org_subs.up.sql ├── 49_add_payment_gateway_events.down.sql ├── 49_add_payment_gateway_events.up.sql ├── 4_add_avatar_url_to_users.down.sql ├── 4_add_avatar_url_to_users.up.sql ├── 50_add_user_id_to_payment_gateway_events.down.sql ├── 50_add_user_id_to_payment_gateway_events.up.sql ├── 51_add_index_to_org_subs.down.sql ├── 51_add_index_to_org_subs.up.sql ├── 52_add_payment_gateway_name_to_org_subs.down.sql ├── 52_add_payment_gateway_name_to_org_subs.up.sql ├── 53_add_version_to_org_and_org_subs.down.sql ├── 53_add_version_to_org_and_org_subs.up.sql ├── 54_add_price_per_seat_to_org_subs.down.sql ├── 54_add_price_per_seat_to_org_subs.up.sql ├── 55_add_cancel_url_to_org_subs.down.sql ├── 55_add_cancel_url_to_org_subs.up.sql ├── 56_add_is_private_to_repos.down.sql ├── 56_add_is_private_to_repos.up.sql ├── 57_add_create_fail_reason_to_repos.down.sql ├── 57_add_create_fail_reason_to_repos.up.sql ├── 58_add_is_empty_to_repo_analysis_statuses.down.sql ├── 58_add_is_empty_to_repo_analysis_statuses.up.sql ├── 59_add_index_to_pull_request_analyzes.down.sql ├── 59_add_index_to_pull_request_analyzes.up.sql ├── 5_add_login_to_github_auths.down.sql ├── 5_add_login_to_github_auths.up.sql ├── 6_drop_nickname_from_users.down.sql ├── 6_drop_nickname_from_users.up.sql ├── 7_fix_uniqs_int_github_auths.down.sql ├── 7_fix_uniqs_int_github_auths.up.sql ├── 8_add_github_repos.down.sql ├── 8_add_github_repos.up.sql ├── 9_add_hook_id_to_github_repos.down.sql └── 9_add_hook_id_to_github_repos.up.sql ├── pkg ├── api │ ├── app.go │ ├── app_modifiers.go │ ├── auth │ │ ├── authorizer.go │ │ └── oauth │ │ │ ├── factory.go │ │ │ └── oauth.go │ ├── crons │ │ ├── pranalyzes │ │ │ ├── reanalyzer.go │ │ │ └── staler.go │ │ └── repoinfo │ │ │ └── updater.go │ ├── hooks │ │ └── injector.go │ ├── models │ │ ├── auth.go │ │ ├── autogenerated_auth.go │ │ ├── autogenerated_org.go │ │ ├── autogenerated_org_sub.go │ │ ├── autogenerated_payment_gateway_event.go │ │ ├── autogenerated_pull_request_analysis.go │ │ ├── autogenerated_repo.go │ │ ├── autogenerated_repo_analysis.go │ │ ├── autogenerated_repo_analysis_status.go │ │ ├── autogenerated_user.go │ │ ├── org.go │ │ ├── org_sub.go │ │ ├── payment_gateway_event.go │ │ ├── pull_request_analysis.go │ │ ├── repo.go │ │ ├── repo_analysis.go │ │ ├── repo_analysis_status.go │ │ └── user.go │ ├── policy │ │ ├── active_subscription.go │ │ ├── errors.go │ │ ├── org_fetcher.go │ │ ├── organization.go │ │ └── repo.go │ ├── request │ │ ├── body.go │ │ ├── context.go │ │ ├── org.go │ │ └── repo.go │ ├── returntypes │ │ └── returntypes.go │ ├── services │ │ ├── auth │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ ├── events │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ ├── organization │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ ├── pranalysis │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ ├── repo │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ ├── repoanalysis │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ ├── repohook │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ │ └── subscription │ │ │ ├── endpoint.go │ │ │ ├── service.go │ │ │ └── transport.go │ └── workers │ │ └── primaryqueue │ │ ├── config.go │ │ ├── helpers.go │ │ ├── invitations │ │ └── acceptor.go │ │ ├── paymentevents │ │ └── creator.go │ │ ├── repoanalyzes │ │ └── launcher.go │ │ ├── repos │ │ ├── collaborator_util.go │ │ ├── creator.go │ │ └── deleter.go │ │ └── subs │ │ ├── creator.go │ │ ├── deleter.go │ │ └── updater.go ├── buildagent │ ├── build │ │ └── runner.go │ └── containers │ │ └── orchestrator.go ├── goenvbuild │ ├── command │ │ └── runner.go │ ├── config │ │ └── config.go │ ├── ensuredeps │ │ └── ensure.go │ ├── logger │ │ └── logger.go │ ├── packages │ │ ├── exclude.go │ │ ├── package.go │ │ ├── program.go │ │ ├── resolver.go │ │ └── resolver_test.go │ ├── preparer.go │ ├── repoinfo │ │ ├── fetch.go │ │ └── info.go │ └── result │ │ ├── result.go │ │ └── run.go └── worker │ ├── analytics │ ├── amplitude.go │ ├── context.go │ ├── errors.go │ ├── logger.go │ ├── mixpanel.go │ └── track.go │ ├── analyze │ ├── analyzequeue │ │ ├── consumers │ │ │ ├── analyze_pr.go │ │ │ ├── analyze_pr_test.go │ │ │ ├── analyze_repo.go │ │ │ ├── base_consumer.go │ │ │ └── errors.go │ │ └── task │ │ │ └── task.go │ ├── analyzesqueue │ │ ├── config.go │ │ ├── helpers.go │ │ ├── pullanalyzesqueue │ │ │ ├── config.go │ │ │ ├── consumer.go │ │ │ └── producer.go │ │ └── repoanalyzesqueue │ │ │ ├── config.go │ │ │ ├── consumer.go │ │ │ └── producer.go │ ├── linters │ │ ├── golinters │ │ │ └── golangci_lint.go │ │ ├── linter.go │ │ ├── linter_mock.go │ │ ├── result │ │ │ ├── issue.go │ │ │ └── result.go │ │ └── runner.go │ ├── logger │ │ └── logger.go │ ├── processors │ │ ├── basic_pull_processor.go │ │ ├── basic_pull_processor_factory.go │ │ ├── basic_pull_processor_test.go │ │ ├── def.go │ │ ├── errors.go │ │ ├── escape.go │ │ ├── executor.go │ │ ├── nop_processor.go │ │ ├── pull_processor.go │ │ ├── pull_processor_factory.go │ │ ├── repo_processor.go │ │ ├── repo_processor_factory.go │ │ ├── result.go │ │ └── test │ │ │ ├── 1.patch │ │ │ ├── main0.go │ │ │ └── main1.go │ ├── prstate │ │ ├── api_storage.go │ │ ├── storage.go │ │ └── storage_mock.go │ ├── reporters │ │ ├── github_reviewer.go │ │ ├── reporter.go │ │ └── reporter_mock.go │ ├── repostate │ │ ├── api_storage.go │ │ ├── storage.go │ │ └── storage_mock.go │ └── resources │ │ └── requirements.go │ ├── app │ ├── app.go │ ├── app_modifiers.go │ └── app_test.go │ ├── lib │ ├── errorutils │ │ └── errors.go │ ├── executors │ │ ├── container.go │ │ ├── env.go │ │ ├── executor.go │ │ ├── executor_mock.go │ │ ├── fargate.go │ │ ├── shell.go │ │ ├── temp_dir_shell.go │ │ └── temp_dir_shell_test.go │ ├── experiments │ │ └── checker.go │ ├── fetchers │ │ ├── fetcher.go │ │ ├── fetcher_mock.go │ │ ├── git.go │ │ ├── git_test.go │ │ └── repo.go │ ├── github │ │ ├── client.go │ │ ├── client_mock.go │ │ └── context.go │ ├── goutils │ │ ├── environments │ │ │ ├── environment.go │ │ │ └── golang.go │ │ └── workspaces │ │ │ ├── go.go │ │ │ └── workspace.go │ ├── httputils │ │ ├── client.go │ │ └── client_mock.go │ ├── runmode │ │ └── runmode.go │ └── timeutils │ │ └── track.go │ ├── scripts │ └── cleanup.sh │ └── test │ ├── env.go │ └── linters.go ├── scripts ├── build_email_campaign │ └── main.go ├── consume_dlq │ └── main.go ├── decode_cookie │ └── main.go ├── emulate_webhook │ └── main.go ├── import_amplitude │ └── main.go ├── print_last_reported_issues │ └── main.go ├── reanalyze_repo │ └── main.go └── recover_pull_analyzes │ └── main.go └── test ├── activate_test.go ├── data └── github_fake_response │ ├── add_hook.json │ ├── get_branch │ └── golangci │ │ └── golangci-api │ │ └── master.json │ ├── get_profile.json │ ├── get_repo │ └── golangci │ │ └── golangci-api.json │ ├── get_repo_hooks │ └── golangci │ │ └── golangci-api.json │ ├── list_repo_page1.json │ └── list_repo_page2.json ├── events_test.go ├── github_login_test.go ├── hooks_test.go ├── list_test.go ├── prepare_env.sh └── sharedtest ├── app.go ├── auth.go ├── common_deps.go ├── default_app.go ├── github_fake.go ├── mocks └── provider_factory.go └── repos.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.13 6 | - image: redis 7 | - image: circleci/postgres:9.4 8 | environment: 9 | POSTGRES_USER: postgres 10 | POSTGRES_DB: api_test 11 | POSTGRES_HOST_AUTH_METHOD: trust 12 | 13 | working_directory: /go/src/github.com/golangci/golangci-api 14 | steps: 15 | - checkout 16 | - run: go get github.com/golangci/golangci-lint/cmd/golangci-lint 17 | - run: make prepare_test_env 18 | - run: 19 | name: install dockerize 20 | command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz 21 | environment: 22 | DOCKERIZE_VERSION: v0.3.0 23 | - run: 24 | name: Wait for db 25 | command: dockerize -wait tcp://localhost:5432 -timeout 1m 26 | - run: ./.circleci/lock.sh make test 27 | build_docker_image: 28 | machine: true 29 | steps: 30 | - checkout 31 | - run: docker login -u $DOCKER_USER -p $DOCKER_PASS 32 | - run: git clone $DEPLOYMENTS_REPO ~/deploy 33 | 34 | - run: ./.circleci/lock.sh --branch master make build_docker_images 35 | 36 | workflows: 37 | version: 2 38 | build-master: 39 | jobs: 40 | - build 41 | - build_docker_image: 42 | requires: 43 | - build 44 | filters: 45 | branches: 46 | only: master 47 | 48 | -------------------------------------------------------------------------------- /.circleci/lock.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # https://github.com/bellkev/circle-lock-test/blob/master/do-exclusively 3 | 4 | # sets $branch, $tag, $rest 5 | parse_args() { 6 | while [[ $# -gt 0 ]]; do 7 | case $1 in 8 | -b|--branch) branch="$2" ;; 9 | -t|--tag) tag="$2" ;; 10 | *) break ;; 11 | esac 12 | shift 2 13 | done 14 | rest=("$@") 15 | } 16 | 17 | # reads $branch, $tag, $commit_message 18 | should_skip() { 19 | if [[ "$branch" && "$CIRCLE_BRANCH" != "$branch" ]]; then 20 | echo "Not on branch $branch. Skipping..." 21 | return 0 22 | fi 23 | 24 | if [[ "$tag" && "$commit_message" != *\[$tag\]* ]]; then 25 | echo "No [$tag] commit tag found. Skipping..." 26 | return 0 27 | fi 28 | 29 | return 1 30 | } 31 | 32 | # reads $branch, $tag 33 | # sets $jq_prog 34 | make_jq_prog() { 35 | local jq_filters="" 36 | 37 | if [[ $branch ]]; then 38 | jq_filters+=" and .branch == \"$branch\"" 39 | fi 40 | 41 | if [[ $tag ]]; then 42 | jq_filters+=" and (.subject | contains(\"[$tag]\"))" 43 | fi 44 | 45 | jq_prog=".[] | select(.build_num < $CIRCLE_BUILD_NUM and (.status | test(\"running|pending|queued\")) $jq_filters) | .build_num" 46 | } 47 | 48 | 49 | if [[ "$0" != *bats* ]]; then 50 | set -e 51 | set -u 52 | set -o pipefail 53 | 54 | branch="" 55 | tag="" 56 | rest=() 57 | api_url="https://circleci.com/api/v1/project/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME?circle-token=$CIRCLE_TOKEN&limit=100" 58 | 59 | parse_args "$@" 60 | commit_message=$(git log -1 --pretty=%B) 61 | if should_skip; then exit 0; fi 62 | make_jq_prog 63 | 64 | echo "Checking for running builds..." 65 | 66 | while true; do 67 | builds=$(curl -s -H "Accept: application/json" "$api_url" | jq "$jq_prog") 68 | if [[ $builds ]]; then 69 | echo "Waiting on builds:" 70 | echo "$builds" 71 | else 72 | break 73 | fi 74 | echo "Retrying in 5 seconds..." 75 | sleep 5 76 | done 77 | 78 | echo "Acquired lock" 79 | 80 | if [[ "${#rest[@]}" -ne 0 ]]; then 81 | "${rest[@]}" 82 | fi 83 | fi -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /.env.* 3 | /vendor 4 | /golangci-api 5 | /*.log 6 | /logs/ 7 | /tmp/ 8 | /gin-bin 9 | /*.token 10 | /*.json 11 | /*.txt 12 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 2m 3 | 4 | linters-settings: 5 | govet: 6 | check-shadowing: true 7 | golint: 8 | min-confidence: 0 9 | gocyclo: 10 | min-complexity: 10 11 | maligned: 12 | suggest-new: true 13 | dupl: 14 | threshold: 100 15 | goconst: 16 | min-len: 2 17 | min-occurrences: 2 18 | lll: 19 | line-length: 140 20 | gocritic: 21 | disabled-checks: 22 | - ifElseChain 23 | - singleCaseSwitch 24 | 25 | linters: 26 | enable-all: true 27 | disable: 28 | - maligned 29 | - lll 30 | - prealloc 31 | - gosec 32 | - gochecknoglobals 33 | - gochecknoinits 34 | - scopelint 35 | - dupl 36 | - interfacer 37 | - wsl 38 | - godox 39 | - funlen 40 | - whitespace 41 | 42 | # golangci.com configuration 43 | # https://github.com/golangci/golangci/wiki/Configuration 44 | service: 45 | golangci-lint-version: 1.23.x # use fixed version to not introduce new linters unexpectedly 46 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/golangci/golangci-lint 3 | rev: v1.20.0 4 | hooks: 5 | - id: golangci-lint 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: golangci-api 2 | worker: golangci-worker -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/golangci/golangci-api.svg?style=svg)](https://circleci.com/gh/golangci/golangci-api) 2 | [![GolangCI](https://golangci.com/badges/github.com/golangci/golangci-api.svg)](https://golangci.com) 3 | 4 | # API 5 | This repository contains code of API. 6 | 7 | ## Development 8 | ### Technologies 9 | Go (golang), heroku, circleci, docker, redis, postgres. 10 | Web framework is a go kit wrapped with a [code generation](https://github.com/golangci/golangci-api/blob/master/cmd/genservices/main.go). 11 | 12 | ### Preparation 13 | Run: 14 | ``` 15 | docker-compose up -d 16 | echo "create database api_prod;" | docker-compose exec -T pg psql -Upostgres 17 | ``` 18 | It runs postgres and redis needed for both api and worker. 19 | 20 | ### How to run 21 | ```bash 22 | make run_api 23 | make run_worker 24 | ``` 25 | 26 | ### Configuration 27 | Configurate via `.env` file. Dev `.env` may be like this: 28 | ``` 29 | WEB_ROOT="https://dev.golangci.com" 30 | API_URL="https://api.dev.golangci.com" 31 | GITHUB_CALLBACK_HOST=https://api.dev.golangci.com 32 | DATABASE_URL="postgresql://postgres:test@localhost:5432/api_prod?sslmode=disable" 33 | REDIS_URL="redis://127.0.0.1:6379" 34 | PORT=3000 35 | APP_NAME="GolangCI Dev" 36 | ``` 37 | 38 | Tests need `.env.test` file, overriding options from `.env`. There can be something like this: 39 | ``` 40 | DATABASE_URL="postgresql://postgres:test@localhost:5432/api_test?sslmode=disable" 41 | DATABASE_DEBUG=1 42 | ``` 43 | 44 | ### How to run tests 45 | ``` 46 | echo "CREATE DATABASE api_test;" | docker-compose exec -T pg psql -U postgres 47 | make test 48 | ``` 49 | 50 | ### How to test with web 51 | Run golangci-web, golangci-worker and golangci-api. Go to `https://dev.golangci.com` locally and it will work. 52 | 53 | ## Subscriptions and Payment Gateway 54 | 55 | ### Requirements 56 | 57 | To use Subscriptions you will need to configure the env variables for the gateway of your choice. 58 | 59 | * Note: Currently only SecurionPay is supported and uses `SECURIONPAY_SECRET` and `SECURIONPAY_PLANID`. 60 | 61 | ### Payment Gateway Callbacks 62 | 63 | Run `ngrok http 3000` on your development machine, and use `https://{ngrok_id}.ngrok.io/v1/payment/{gateway}/events` as URL to receive events from the payment gateway. 64 | 65 | * `{gateway}` for SecurionPay is `securionpay`. 66 | * `{ngrok_id}`'s are unique and you must update the callback URL when you restart Ngrok service. 67 | 68 | # Contributing 69 | See [CONTRIBUTING](https://github.com/golangci/golangci-api/blob/master/CONTRIBUTING.md). 70 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exu 4 | 5 | # if [[ `git status --porcelain` ]]; then 6 | # echo has git changes; 7 | # echo exit 1; 8 | # fi 9 | 10 | if [ $1 = "api" ]; then 11 | GO111MODULE=on GOOS=linux CGO_ENABLED=0 GOARCH=amd64 \ 12 | go build -o golangci-api-dlq-consumer \ 13 | ./scripts/consume_dlq/main.go 14 | GO111MODULE=on GOOS=linux CGO_ENABLED=0 GOARCH=amd64 \ 15 | go build -o golangci-api-pull-analyzes-recover \ 16 | ./scripts/recover_pull_analyzes/main.go 17 | GO111MODULE=on GOOS=linux CGO_ENABLED=0 GOARCH=amd64 \ 18 | go build -o golangci-api-build-email-campaign \ 19 | ./scripts/build_email_campaign/main.go 20 | fi 21 | 22 | GO111MODULE=on GOOS=linux CGO_ENABLED=0 GOARCH=amd64 \ 23 | go build \ 24 | -ldflags "-s -w -X 'main.version=$(git name-rev --tags --name-only $(git rev-parse HEAD))' -X 'main.commit=$(git rev-parse --short HEAD)' -X 'main.date=$(date)'" \ 25 | -o golangci-${1} \ 26 | ./cmd/golangci-${1}/main.go 27 | -------------------------------------------------------------------------------- /cmd/buildrunner/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/config" 7 | "github.com/golangci/golangci-api/internal/shared/logutil" 8 | "github.com/golangci/golangci-api/pkg/buildagent/build" 9 | ) 10 | 11 | func main() { 12 | log := logutil.NewStderrLog("runner") 13 | log.SetLevel(logutil.LogLevelInfo) 14 | cfg := config.NewEnvConfig(log) 15 | 16 | // shutdown server after maxLifetime to prevent staling containers 17 | // eating all system resources 18 | token := cfg.GetString("TOKEN") 19 | r := build.NewRunner(log, token) 20 | 21 | maxLifetime := cfg.GetDuration("MAX_LIFETIME", 15*time.Minute) 22 | port := cfg.GetInt("PORT", 7000) 23 | if err := r.Run(port, maxLifetime); err != nil { 24 | log.Warnf("Runner error: %s", err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/containers_orchestrator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/config" 5 | "github.com/golangci/golangci-api/internal/shared/logutil" 6 | "github.com/golangci/golangci-api/pkg/buildagent/containers" 7 | ) 8 | 9 | func main() { 10 | log := logutil.NewStderrLog("orchestrator") 11 | log.SetLevel(logutil.LogLevelInfo) 12 | cfg := config.NewEnvConfig(log) 13 | 14 | // shutdown server after maxLifetime to prevent staling containers 15 | // eating all system resources 16 | token := cfg.GetString("TOKEN") 17 | r := containers.NewOrchestrator(log, token) 18 | 19 | port := cfg.GetInt("PORT", 8001) 20 | if err := r.Run(port); err != nil { 21 | log.Warnf("Orchestrator running error: %s", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/ensuredeps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "log" 8 | "os" 9 | 10 | "github.com/golangci/golangci-api/internal/shared/logutil" 11 | "github.com/golangci/golangci-api/pkg/goenvbuild/command" 12 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 13 | 14 | "github.com/golangci/golangci-api/pkg/goenvbuild/ensuredeps" 15 | ) 16 | 17 | func main() { 18 | repoName := flag.String("repo", "", "repo name or path") 19 | flag.Parse() 20 | if *repoName == "" { 21 | log.Fatalf("Repo name must be set: use --repo") 22 | } 23 | 24 | logger := logutil.NewStderrLog("") 25 | logger.SetLevel(logutil.LogLevelInfo) 26 | resLog := result.NewLog(log.New(os.Stdout, "", 0)) 27 | resLog.AddStepGroup("group").AddStep("step") 28 | runner := command.NewStreamingRunner(resLog) 29 | 30 | r := ensuredeps.NewRunner(logger, runner) 31 | ret := r.Run(context.Background(), *repoName) 32 | if err := json.NewEncoder(os.Stdout).Encode(ret); err != nil { 33 | log.Fatalf("Failed to JSON output result: %s", err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/getrepoinfo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "log" 7 | "os" 8 | 9 | "github.com/golangci/golangci-api/internal/shared/logutil" 10 | 11 | "github.com/golangci/golangci-api/pkg/goenvbuild/repoinfo" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func main() { 16 | repo := flag.String("repo", "", "repo path") 17 | flag.Parse() 18 | if *repo == "" { 19 | log.Fatal("Set --repo flag") 20 | } 21 | 22 | if err := printRepoInfo(*repo); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | 27 | func printRepoInfo(repo string) error { 28 | var ret interface{} 29 | log := logutil.NewStderrLog("") 30 | log.SetLevel(logutil.LogLevelInfo) 31 | info, err := repoinfo.Fetch(repo, log) 32 | if err != nil { 33 | ret = struct { 34 | Error string 35 | }{ 36 | Error: err.Error(), 37 | } 38 | } else { 39 | ret = info 40 | } 41 | 42 | if err = json.NewEncoder(os.Stdout).Encode(ret); err != nil { 43 | return errors.Wrap(err, "can't json marshal") 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd/gocodescore/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os/exec" 8 | 9 | "github.com/golangci/golangci-api/internal/api/score" 10 | 11 | "github.com/golangci/golangci-lint/pkg/printers" 12 | ) 13 | 14 | func main() { 15 | cmd := exec.Command("golangci-lint", "run", "--out-format=json", "--issues-exit-code=0") 16 | out, err := cmd.Output() 17 | if err != nil { 18 | log.Fatalf("Failed to run golangci-lint: %s", err) 19 | } 20 | 21 | var runRes printers.JSONResult 22 | if err = json.Unmarshal(out, &runRes); err != nil { 23 | log.Fatalf("Failed to json unmarshal golangci-lint output %s: %s", string(out), err) 24 | } 25 | 26 | calcRes := score.Calculator{}.Calc(&runRes) 27 | fmt.Printf("Score: %d/%d\n", calcRes.Score, calcRes.MaxScore) 28 | if len(calcRes.Recommendations) != 0 { 29 | for _, rec := range calcRes.Recommendations { 30 | fmt.Printf(" - get %d more score: %s\n", rec.ScoreIncrease, rec.Text) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/goenvbuild/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/config" 5 | "github.com/golangci/golangci-api/internal/shared/logutil" 6 | goenv "github.com/golangci/golangci-api/pkg/goenvbuild" 7 | ) 8 | 9 | func main() { 10 | log := logutil.NewStderrLog("config") 11 | cfg := config.NewEnvConfig(log) 12 | p := goenv.NewPreparer(cfg) 13 | p.RunAndPrint() 14 | } 15 | -------------------------------------------------------------------------------- /cmd/golangci-api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | app "github.com/golangci/golangci-api/pkg/api" 9 | ) 10 | 11 | var ( 12 | // Populated during a build 13 | version = "" 14 | commit = "" 15 | date = "" 16 | ) 17 | 18 | func main() { 19 | printVersion := flag.Bool("version", false, "print version") 20 | flag.Parse() 21 | 22 | if *printVersion { 23 | fmt.Printf("golangci-api has version %s built from %s on %s\n", version, commit, date) 24 | os.Exit(0) 25 | } 26 | 27 | app := app.NewApp() 28 | app.RunForever() 29 | } 30 | -------------------------------------------------------------------------------- /cmd/golangci-worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/pkg/worker/app" 5 | ) 6 | 7 | func main() { 8 | a := app.NewApp() 9 | a.Run() 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pg: 5 | image: postgres 6 | volumes: 7 | - pg_data:/var/lib/postgresql/data 8 | environment: 9 | - POSTGRES_PASSWORD=test 10 | ports: 11 | - 127.0.0.1:5432:5432 12 | 13 | redis: 14 | image: redis 15 | volumes: 16 | - redis_data:/data 17 | ports: 18 | - 127.0.0.1:6379:6379 19 | 20 | volumes: 21 | pg_data: 22 | driver: local 23 | redis_data: 24 | driver: local 25 | -------------------------------------------------------------------------------- /internal/api/endpointutil/handler.go: -------------------------------------------------------------------------------- 1 | package endpointutil 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/apperrors" 5 | "github.com/golangci/golangci-api/internal/shared/config" 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | "github.com/golangci/golangci-api/pkg/api/auth" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | type HandlerRegContext struct { 12 | Authorizer *auth.Authorizer 13 | Log logutil.Log 14 | ErrTracker apperrors.Tracker 15 | Cfg config.Config 16 | DB *gorm.DB 17 | } 18 | -------------------------------------------------------------------------------- /internal/api/events/amplitude.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | amplitude "github.com/savaki/amplitude-go" 8 | ) 9 | 10 | var amplitudeClient *amplitude.Client 11 | var amplitudeClientOnce sync.Once 12 | 13 | func GetAmplitudeClient() *amplitude.Client { 14 | amplitudeClientOnce.Do(func() { 15 | apiKey := os.Getenv("AMPLITUDE_API_KEY") 16 | if apiKey != "" { 17 | amplitudeClient = amplitude.New(apiKey) 18 | } 19 | }) 20 | 21 | return amplitudeClient 22 | } 23 | -------------------------------------------------------------------------------- /internal/api/events/mixpanel.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/dukex/mixpanel" 8 | ) 9 | 10 | var mixpanelClient mixpanel.Mixpanel 11 | var mixpanelClientOnce sync.Once 12 | 13 | func GetMixpanelClient() mixpanel.Mixpanel { 14 | mixpanelClientOnce.Do(func() { 15 | apiKey := os.Getenv("MIXPANEL_API_KEY") 16 | if apiKey != "" { 17 | mixpanelClient = mixpanel.New(apiKey, "") 18 | } 19 | }) 20 | 21 | return mixpanelClient 22 | } 23 | -------------------------------------------------------------------------------- /internal/api/events/tracker.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/dukex/mixpanel" 8 | amplitude "github.com/savaki/amplitude-go" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func NewAuthenticatedTracker(userID int) AuthenticatedTracker { 13 | return AuthenticatedTracker{ 14 | userID: userID, 15 | } 16 | } 17 | 18 | type AuthenticatedTracker struct { 19 | userID int 20 | userProps map[string]interface{} 21 | } 22 | 23 | func (t AuthenticatedTracker) WithUserProps(props map[string]interface{}) AuthenticatedTracker { 24 | tc := t 25 | tc.userProps = props 26 | return tc 27 | } 28 | 29 | func (t AuthenticatedTracker) Track(ctx context.Context, eventName string, props map[string]interface{}) { 30 | eventProps := map[string]interface{}{} 31 | for k, v := range props { 32 | eventProps[k] = v 33 | } 34 | 35 | logrus.Infof("track event %s with props %+v", eventName, eventProps) 36 | 37 | userIDString := strconv.Itoa(t.userID) 38 | ac := GetAmplitudeClient() 39 | if ac != nil { 40 | ev := amplitude.Event{ 41 | UserId: userIDString, 42 | EventType: eventName, 43 | EventProperties: eventProps, 44 | UserProperties: t.userProps, 45 | } 46 | if err := ac.Publish(ev); err != nil { 47 | logrus.Warnf("Can't publish %+v to amplitude: %s", ev, err) 48 | } 49 | } 50 | 51 | mp := GetMixpanelClient() 52 | if mp != nil { 53 | const ip = "0" // don't auto-detect 54 | ev := &mixpanel.Event{ 55 | IP: ip, 56 | Properties: eventProps, 57 | } 58 | if err := mp.Track(userIDString, eventName, ev); err != nil { 59 | logrus.Warnf("Can't publish event %s (%+v) to mixpanel: %s", eventName, ev, err) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/factory.go: -------------------------------------------------------------------------------- 1 | package paymentproviders 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/golangci/golangci-api/internal/api/paymentproviders/implementations/paddle" 8 | "github.com/golangci/golangci-api/internal/shared/config" 9 | 10 | "github.com/golangci/golangci-api/internal/api/paymentproviders/implementations" 11 | "github.com/golangci/golangci-api/internal/api/paymentproviders/paymentprovider" 12 | "github.com/golangci/golangci-api/internal/shared/logutil" 13 | ) 14 | 15 | type Factory interface { 16 | Build(provider string) (paymentprovider.Provider, error) 17 | } 18 | 19 | type basicFactory struct { 20 | log logutil.Log 21 | cfg config.Config 22 | } 23 | 24 | func NewBasicFactory(log logutil.Log, cfg config.Config) Factory { 25 | return &basicFactory{ 26 | log: log, 27 | cfg: cfg, 28 | } 29 | } 30 | 31 | func (f basicFactory) buildImpl(provider string) (paymentprovider.Provider, error) { 32 | switch provider { 33 | case implementations.SecurionPayProviderName: 34 | return implementations.NewSecurionPay(f.log), nil 35 | case paddle.ProviderName: 36 | return paddle.NewProvider(f.log, f.cfg) 37 | default: 38 | return nil, fmt.Errorf("invalid provider name %q", provider) 39 | } 40 | 41 | } 42 | 43 | func (f *basicFactory) Build(provider string) (paymentprovider.Provider, error) { 44 | p, err := f.buildImpl(provider) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return implementations.NewStableProvider(p, time.Second*30, 3), nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/implementations/paddle/const.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | const ProviderName = "paddle" 4 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/implementations/paddle/form_util.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/gorilla/schema" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func structToGrequestsData(s interface{}) (map[string]string, error) { 12 | form := url.Values{} 13 | enc := schema.NewEncoder() 14 | if err := enc.Encode(s, form); err != nil { 15 | return nil, errors.Wrap(err, "failed to encode struct to form") 16 | } 17 | 18 | ret := map[string]string{} 19 | for key, values := range form { 20 | if len(values) != 1 { 21 | return nil, fmt.Errorf("invalid count (%d) of key %s values: %v", len(values), key, values) 22 | } 23 | 24 | ret[key] = values[0] 25 | } 26 | 27 | return ret, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/implementations/paddle/request.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | type requestAuth struct { 4 | VendorID int `schema:"vendor_id"` 5 | VendorAuthCode string `schema:"vendor_auth_code"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/paymentprovider/errors.go: -------------------------------------------------------------------------------- 1 | package paymentprovider 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotFound = errors.New("not found in provider") 7 | ErrInvalidCardToken = errors.New("invalid card token") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/paymentprovider/event_processor.go: -------------------------------------------------------------------------------- 1 | package paymentprovider 2 | 3 | type EventProcessor interface { 4 | Process(payload string, eventUUID string) error 5 | } 6 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/paymentprovider/models.go: -------------------------------------------------------------------------------- 1 | package paymentprovider 2 | 3 | import "encoding/json" 4 | 5 | type SubscriptionStatus string 6 | 7 | const ( 8 | SubscriptionStatusTrialing SubscriptionStatus = "trialing" 9 | SubscriptionStatusActive SubscriptionStatus = "active" 10 | SubscriptionStatusPastDue SubscriptionStatus = "past_due" 11 | SubscriptionStatusCancelled SubscriptionStatus = "cancelled" 12 | SubscriptionStatusUnpaid SubscriptionStatus = "unpaid" 13 | ) 14 | 15 | type Customer struct { 16 | ID string 17 | Email string 18 | } 19 | 20 | type Subscription struct { 21 | ID string 22 | Status SubscriptionStatus 23 | } 24 | 25 | type Event struct { 26 | ID string 27 | Type string 28 | Data json.RawMessage 29 | } 30 | 31 | type SubscriptionUpdatePayload struct { 32 | CardToken string 33 | SeatsCount int 34 | } 35 | 36 | type CustomerUpdatePayload struct { 37 | } 38 | -------------------------------------------------------------------------------- /internal/api/paymentproviders/paymentprovider/provider.go: -------------------------------------------------------------------------------- 1 | package paymentprovider 2 | 3 | import "context" 4 | 5 | type Provider interface { 6 | Name() string 7 | 8 | SetBaseURL(url string) error 9 | 10 | CreateCustomer(ctx context.Context, email string, token string) (*Customer, error) 11 | 12 | GetSubscription(ctx context.Context, cust string, sub string) (*Subscription, error) 13 | GetSubscriptions(ctx context.Context, cust string) ([]Subscription, error) 14 | CreateSubscription(ctx context.Context, cust string, seats int) (*Subscription, error) 15 | UpdateSubscription(ctx context.Context, cust string, sub string, payload SubscriptionUpdatePayload) (*Subscription, error) 16 | DeleteSubscription(ctx context.Context, cust string, sub string) error 17 | 18 | GetEvent(ctx context.Context, eventID string) (*Event, error) 19 | } 20 | -------------------------------------------------------------------------------- /internal/api/session/factory.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | "github.com/golangci/golangci-api/internal/shared/config" 8 | "github.com/pkg/errors" 9 | redistore "gopkg.in/boj/redistore.v1" 10 | ) 11 | 12 | type Factory struct { 13 | store *redistore.RediStore 14 | cfg config.Config 15 | } 16 | 17 | func NewFactory(redisPool *redis.Pool, cfg config.Config, maxAge time.Duration) (*Factory, error) { 18 | sessSecret := cfg.GetString("SESSION_SECRET") 19 | if sessSecret == "" { 20 | return nil, errors.New("SESSION_SECRET isn't set") 21 | } 22 | 23 | store, err := redistore.NewRediStoreWithPool(redisPool, []byte(sessSecret)) 24 | if err != nil { 25 | return nil, errors.Wrap(err, "can't create redis session store") 26 | } 27 | 28 | store.SetMaxAge(int(maxAge / time.Second)) 29 | store.SetSerializer(redistore.JSONSerializer{}) 30 | 31 | f := Factory{ 32 | store: store, 33 | cfg: cfg, 34 | } 35 | f.updateOptions() 36 | 37 | return &f, nil 38 | } 39 | 40 | func (f *Factory) updateOptions() { 41 | f.store.Options.Domain = f.cfg.GetString("COOKIE_DOMAIN") 42 | // TODO: set httponly and secure for non-testing 43 | } 44 | 45 | func (f *Factory) Build(ctx *RequestContext, sessType string) (*Session, error) { 46 | f.updateOptions() // cfg could have changed 47 | 48 | gs, err := ctx.Registry.Get(f.store, sessType) 49 | if err != nil { 50 | return nil, errors.Wrap(err, "failed to get session") 51 | } 52 | 53 | return &Session{ 54 | gs: gs, 55 | saver: ctx.Saver, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/api/session/request_context.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/gorilla/sessions" 5 | ) 6 | 7 | type RequestContext struct { 8 | Saver *Saver 9 | Registry *sessions.Registry 10 | } 11 | -------------------------------------------------------------------------------- /internal/api/session/saver.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | "github.com/gorilla/sessions" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type Saver struct { 12 | sessions []*sessions.Session 13 | log logutil.Log 14 | } 15 | 16 | func NewSaver(log logutil.Log) *Saver { 17 | return &Saver{ 18 | log: log, 19 | } 20 | } 21 | 22 | func (s *Saver) Save(sess *sessions.Session) { 23 | s.sessions = append(s.sessions, sess) 24 | } 25 | 26 | func (s Saver) FinalizeHTTP(r *http.Request, w http.ResponseWriter) error { 27 | for _, sess := range s.sessions { 28 | if err := sess.Save(r, w); err != nil { 29 | return errors.Wrapf(err, "can't finalize session saving for sess %#v", sess) 30 | } 31 | s.log.Infof("Session finalization: url=%s: saved session %#v", r.URL.String(), sess.Values) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/api/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gorilla/sessions" 7 | ) 8 | 9 | type Session struct { 10 | gs *sessions.Session 11 | saver *Saver 12 | } 13 | 14 | func (s Session) GoString() string { 15 | return fmt.Sprintf("%#v", s.gs.Values) 16 | } 17 | 18 | func (s Session) GetValue(key string) interface{} { 19 | return s.gs.Values[key] 20 | } 21 | 22 | func (s *Session) Set(k string, v interface{}) { 23 | s.gs.Values[k] = v 24 | s.saver.Save(s.gs) 25 | } 26 | 27 | func (s *Session) Delete() { 28 | s.gs.Options.MaxAge = -1 29 | s.gs.Values = make(map[interface{}]interface{}) 30 | s.saver.Save(s.gs) 31 | } 32 | -------------------------------------------------------------------------------- /internal/api/transportutil/encode.go: -------------------------------------------------------------------------------- 1 | package transportutil 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/golangci/golangci-api/pkg/api/returntypes" 9 | ) 10 | 11 | func EncodeError(ctx context.Context, err error, w http.ResponseWriter) { 12 | w.Header().Add("Content-Type", "application/json; charset=UTF-8") 13 | w.WriteHeader(http.StatusBadRequest) 14 | 15 | resp := returntypes.Error{ 16 | Error: err.Error(), 17 | } 18 | 19 | _ = json.NewEncoder(w).Encode(resp) 20 | } 21 | -------------------------------------------------------------------------------- /internal/api/transportutil/log.go: -------------------------------------------------------------------------------- 1 | package transportutil 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-kit/kit/log" 8 | "github.com/golangci/golangci-api/internal/shared/logutil" 9 | ) 10 | 11 | func AdaptErrorLogger(log logutil.Log) log.Logger { 12 | return &errorLog{ 13 | sourceLogger: log, 14 | } 15 | } 16 | 17 | type errorLog struct { 18 | sourceLogger logutil.Log 19 | } 20 | 21 | func (el errorLog) Log(values ...interface{}) error { 22 | parts := []string{} 23 | for _, v := range values { 24 | parts = append(parts, fmt.Sprint(v)) 25 | } 26 | el.sourceLogger.Errorf("gokit transport error: %s", strings.Join(parts, ",")) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/api/transportutil/session.go: -------------------------------------------------------------------------------- 1 | package transportutil 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/golangci/golangci-api/internal/api/endpointutil" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func FinalizeSession(ctx context.Context, w http.ResponseWriter) context.Context { 12 | rc := endpointutil.RequestContext(ctx) 13 | if rc == nil { // was error during request initialization 14 | return ctx 15 | } 16 | 17 | r := getHTTPRequestFromContext(ctx) 18 | sessCtx := rc.SessContext() 19 | if err := sessCtx.Saver.FinalizeHTTP(r, w); err != nil { 20 | rc.Logger().Errorf("Request failed on session finalization: %s", err) 21 | err = errors.Wrap(err, "failed to finalize session") 22 | return storeContextError(ctx, err) 23 | } 24 | 25 | return ctx 26 | } 27 | -------------------------------------------------------------------------------- /internal/api/util/random.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "crypto/rand" 4 | 5 | // GenerateRandomBytes returns securely generated random bytes. 6 | // It will return an error if the system's secure random 7 | // number generator fails to function correctly, in which 8 | // case the caller should not continue. 9 | func generateRandomBytes(n int) ([]byte, error) { 10 | b := make([]byte, n) 11 | _, err := rand.Read(b) 12 | // Note that err == nil only if we read len(b) bytes. 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return b, nil 18 | } 19 | 20 | // GenerateRandomString returns a securely generated random string. 21 | // It will return an error if the system's secure random 22 | // number generator fails to function correctly, in which 23 | // case the caller should not continue. 24 | func GenerateRandomString(n int) (string, error) { 25 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" 26 | bytes, err := generateRandomBytes(n) 27 | if err != nil { 28 | return "", err 29 | } 30 | for i, b := range bytes { 31 | bytes[i] = letters[b%byte(len(letters))] 32 | } 33 | return string(bytes), nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/shared/apperrors/log.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | ) 8 | 9 | func WrapLogWithTracker(log logutil.Log, lctx logutil.Context, t Tracker) logutil.Log { 10 | return trackedLog{ 11 | log: log, 12 | lctx: lctx, 13 | t: t, 14 | } 15 | } 16 | 17 | type trackedLog struct { 18 | log logutil.Log 19 | lctx logutil.Context 20 | t Tracker 21 | } 22 | 23 | func (tl trackedLog) Fatalf(format string, args ...interface{}) { 24 | tl.log.Fatalf(format, args...) 25 | } 26 | 27 | func (tl trackedLog) Errorf(format string, args ...interface{}) { 28 | tl.t.Track(LevelError, fmt.Sprintf(format, args...), tl.lctx) 29 | tl.log.Errorf(format, args...) 30 | } 31 | 32 | func (tl trackedLog) Warnf(format string, args ...interface{}) { 33 | tl.t.Track(LevelWarn, fmt.Sprintf(format, args...), tl.lctx) 34 | tl.log.Warnf(format, args...) 35 | } 36 | 37 | func (tl trackedLog) Infof(format string, args ...interface{}) { 38 | tl.log.Infof(format, args...) 39 | } 40 | 41 | func (tl trackedLog) Debugf(key string, format string, args ...interface{}) { 42 | tl.log.Debugf(key, format, args...) 43 | } 44 | 45 | func (tl trackedLog) Child(name string) logutil.Log { 46 | return tl.log.Child(name) 47 | } 48 | 49 | func (tl trackedLog) SetLevel(level logutil.LogLevel) { 50 | tl.log.SetLevel(level) 51 | } 52 | -------------------------------------------------------------------------------- /internal/shared/apperrors/nop_tracker.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import "net/http" 4 | 5 | type NopTracker struct{} 6 | 7 | func NewNopTracker() *NopTracker { 8 | return &NopTracker{} 9 | } 10 | 11 | func (t NopTracker) Track(level Level, errorText string, ctx map[string]interface{}) { 12 | } 13 | 14 | func (t NopTracker) WithHTTPRequest(r *http.Request) Tracker { 15 | return t 16 | } 17 | -------------------------------------------------------------------------------- /internal/shared/apperrors/rollbar_tracker.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/stvp/rollbar" 9 | ) 10 | 11 | type RollbarTracker struct { 12 | r *http.Request 13 | project string 14 | } 15 | 16 | func NewRollbarTracker(token, project, env string) *RollbarTracker { 17 | rollbar.Environment = env 18 | rollbar.Token = token 19 | 20 | return &RollbarTracker{ 21 | project: project, 22 | } 23 | } 24 | 25 | func (t RollbarTracker) Track(level Level, errorText string, ctx map[string]interface{}) { 26 | fields := []*rollbar.Field{} 27 | 28 | errorParts := strings.SplitN(errorText, ": ", 2) 29 | errorClass := errorParts[0] 30 | if len(errorParts) == 2 { 31 | if ctx == nil { 32 | ctx = map[string]interface{}{} 33 | } 34 | ctx["error_detail"] = errorParts[1] 35 | } 36 | 37 | if ctx != nil { 38 | fields = append(fields, &rollbar.Field{ 39 | Name: "props", 40 | Data: ctx, 41 | }) 42 | } 43 | 44 | fields = append(fields, &rollbar.Field{ 45 | Name: "project", 46 | Data: t.project, 47 | }) 48 | 49 | var rollbarLevel string 50 | switch level { 51 | case LevelError: 52 | rollbarLevel = rollbar.ERR 53 | case LevelWarn: 54 | rollbarLevel = rollbar.WARN 55 | default: 56 | panic("invalid level " + level) 57 | } 58 | 59 | if t.r != nil { 60 | rollbar.RequestError(rollbarLevel, t.r, errors.New(errorClass), fields...) 61 | } else { 62 | rollbar.Error(rollbarLevel, errors.New(errorClass), fields...) 63 | } 64 | } 65 | 66 | func (t RollbarTracker) WithHTTPRequest(r *http.Request) Tracker { 67 | t.r = r 68 | return t 69 | } 70 | -------------------------------------------------------------------------------- /internal/shared/apperrors/sentry_tracker.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | raven "github.com/getsentry/raven-go" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type SentryTracker struct { 13 | r *http.Request 14 | } 15 | 16 | func NewSentryTracker(dsn, env string) (*SentryTracker, error) { 17 | raven.SetEnvironment(env) 18 | err := raven.SetDSN(dsn) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "can't set sentry dsn") 21 | } 22 | 23 | return &SentryTracker{}, nil 24 | } 25 | 26 | func (t SentryTracker) Track(level Level, errorText string, ctx map[string]interface{}) { 27 | tags := map[string]string{} 28 | for k, v := range ctx { 29 | tags[k] = fmt.Sprintf("%v", v) 30 | } 31 | 32 | var interfaces []raven.Interface 33 | if t.r != nil { 34 | interfaces = append(interfaces, raven.NewHttp(t.r)) 35 | } 36 | 37 | errorParts := strings.SplitN(errorText, ": ", 2) 38 | errorClass := errorParts[0] 39 | 40 | p := raven.NewPacket(errorText, interfaces...) 41 | p.Fingerprint = []string{errorClass} 42 | 43 | switch level { 44 | case LevelError: 45 | p.Level = raven.ERROR 46 | case LevelWarn: 47 | p.Level = raven.WARNING 48 | default: 49 | panic("invalid level " + level) 50 | } 51 | 52 | raven.Capture(p, tags) 53 | } 54 | 55 | func (t SentryTracker) WithHTTPRequest(r *http.Request) Tracker { 56 | t.r = r 57 | return t 58 | } 59 | -------------------------------------------------------------------------------- /internal/shared/apperrors/tracker.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import "net/http" 4 | 5 | type Level string 6 | 7 | const ( 8 | LevelError Level = "ERROR" 9 | LevelWarn Level = "WARN" 10 | ) 11 | 12 | type Tracker interface { 13 | Track(level Level, errorText string, ctx map[string]interface{}) 14 | WithHTTPRequest(r *http.Request) Tracker 15 | } 16 | -------------------------------------------------------------------------------- /internal/shared/apperrors/tracker_builder.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/config" 5 | "github.com/golangci/golangci-api/internal/shared/logutil" 6 | ) 7 | 8 | func GetTracker(cfg config.Config, log logutil.Log, project string) Tracker { 9 | env := cfg.GetString("GO_ENV") 10 | 11 | if cfg.GetBool("ROLLBAR_ENABLED", false) { 12 | return NewRollbarTracker(cfg.GetString("ROLLBAR_TOKEN"), project, env) 13 | } 14 | 15 | if cfg.GetBool("SENTRY_ENABLED", false) { 16 | t, err := NewSentryTracker(cfg.GetString("SENTRY_DSN"), env) 17 | if err != nil { 18 | log.Warnf("Can't make sentry error tracker: %s", err) 19 | return NewNopTracker() 20 | } 21 | 22 | return t 23 | } 24 | 25 | return NewNopTracker() 26 | } 27 | -------------------------------------------------------------------------------- /internal/shared/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Cache interface { 8 | Get(key string, dest interface{}) error 9 | Set(key string, expireTimeout time.Duration, value interface{}) error 10 | } 11 | -------------------------------------------------------------------------------- /internal/shared/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/garyburd/redigo/redis" 9 | ) 10 | 11 | const keyPrefix = "cache/" 12 | 13 | type Redis struct { 14 | pool *redis.Pool 15 | } 16 | 17 | func NewRedis(redisURL string) *Redis { 18 | return &Redis{ 19 | pool: &redis.Pool{ 20 | MaxIdle: 10, 21 | IdleTimeout: 240 * time.Second, 22 | TestOnBorrow: func(c redis.Conn, _ time.Time) error { 23 | _, pingErr := c.Do("PING") 24 | return pingErr 25 | }, 26 | Dial: func() (redis.Conn, error) { 27 | return redis.DialURL(redisURL) 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func (r Redis) Get(key string, dest interface{}) error { 34 | key = keyPrefix + key 35 | 36 | conn := r.pool.Get() 37 | defer conn.Close() 38 | 39 | var data []byte 40 | data, err := redis.Bytes(conn.Do("GET", key)) 41 | if err != nil { 42 | if err == redis.ErrNil { 43 | return nil // Cache miss 44 | } 45 | return fmt.Errorf("error getting key %s: %v", key, err) 46 | } 47 | 48 | if err = json.Unmarshal(data, dest); err != nil { 49 | return fmt.Errorf("can't unmarshal json from redis: %s", err) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (r Redis) Set(key string, expireTimeout time.Duration, value interface{}) error { 56 | key = keyPrefix + key 57 | 58 | valueBytes, err := json.Marshal(value) 59 | if err != nil { 60 | return fmt.Errorf("can't json marshal value: %s", err) 61 | } 62 | 63 | conn := r.pool.Get() 64 | defer conn.Close() 65 | 66 | _, err = conn.Do("SETEX", key, int(expireTimeout/time.Second), valueBytes) 67 | if err != nil { 68 | v := string(valueBytes) 69 | if len(v) > 15 { 70 | v = v[0:12] + "..." 71 | } 72 | return fmt.Errorf("error setting key %s to %s: %v", key, v, err) 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/shared/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | type Config interface { 6 | GetString(key string) string 7 | GetStringList(key string) []string 8 | GetDuration(key string, def time.Duration) time.Duration 9 | GetInt(key string, def int) int 10 | GetBool(key string, def bool) bool 11 | } 12 | -------------------------------------------------------------------------------- /internal/shared/config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/golangci/golangci-api/internal/shared/logutil" 10 | ) 11 | 12 | type EnvConfig struct { 13 | log logutil.Log 14 | } 15 | 16 | func NewEnvConfig(log logutil.Log) *EnvConfig { 17 | return &EnvConfig{ 18 | log: log, 19 | } 20 | } 21 | 22 | func (c EnvConfig) GetString(key string) string { 23 | return c.getValue(key) 24 | } 25 | 26 | func (c EnvConfig) GetStringList(key string) []string { 27 | return strings.Split(c.GetString(key), ",") 28 | } 29 | 30 | func (c EnvConfig) getValue(key string) string { 31 | return os.Getenv(strings.ToUpper(key)) 32 | } 33 | 34 | func (c EnvConfig) GetDuration(key string, def time.Duration) time.Duration { 35 | cfgStr := c.getValue(key) 36 | if cfgStr == "" { 37 | return def 38 | } 39 | 40 | d, err := time.ParseDuration(cfgStr) 41 | if err != nil { 42 | c.log.Warnf("Config: invalid %s %q: %s", key, cfgStr, err) 43 | return def 44 | } 45 | 46 | return d 47 | 48 | } 49 | 50 | func (c EnvConfig) GetInt(key string, def int) int { 51 | cfgStr := c.getValue(key) 52 | if cfgStr == "" { 53 | return def 54 | } 55 | 56 | v, err := strconv.Atoi(cfgStr) 57 | if err != nil { 58 | c.log.Warnf("Config: invalid %s %q: %s", key, cfgStr, err) 59 | return def 60 | } 61 | 62 | return v 63 | } 64 | 65 | func (c EnvConfig) GetBool(key string, def bool) bool { 66 | cfgStr := c.getValue(key) 67 | if cfgStr == "" { 68 | return def 69 | } 70 | 71 | if cfgStr == "1" || cfgStr == "true" { 72 | return true 73 | } 74 | 75 | if cfgStr == "0" || cfgStr == "false" { 76 | return false 77 | } 78 | 79 | c.log.Warnf("Config: invalid %s %q", key, cfgStr) 80 | return def 81 | } 82 | -------------------------------------------------------------------------------- /internal/shared/db/gormdb/gormdb.go: -------------------------------------------------------------------------------- 1 | package gormdb 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/golangci/golangci-api/internal/shared/config" 9 | "github.com/golangci/golangci-api/internal/shared/logutil" 10 | "github.com/jinzhu/gorm" 11 | _ "github.com/mattes/migrate/database/postgres" // init pg driver 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func GetDBConnString(cfg config.Config) (string, error) { 16 | dbURL := cfg.GetString("DATABASE_URL") 17 | if dbURL != "" { 18 | dbURL = strings.Replace(dbURL, "postgresql", "postgres", 1) 19 | return dbURL, nil 20 | } 21 | 22 | host := cfg.GetString("DATABASE_HOST") 23 | username := cfg.GetString("DATABASE_USERNAME") 24 | password := cfg.GetString("DATABASE_PASSWORD") 25 | name := cfg.GetString("DATABASE_NAME") 26 | if host == "" || username == "" || password == "" || name == "" { 27 | return "", errors.New("no DATABASE_URL or DATABASE_{HOST,USERNAME,PASSWORD,NAME} in config") 28 | } 29 | 30 | //TODO: enable SSL, but it's not critical 31 | return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", username, password, host, name), nil 32 | } 33 | 34 | func GetDB(cfg config.Config, log logutil.Log, connString string) (*gorm.DB, error) { 35 | if connString == "" { 36 | var err error 37 | connString, err = GetDBConnString(cfg) 38 | if err != nil { 39 | return nil, err 40 | } 41 | } 42 | adapter := strings.Split(connString, "://")[0] 43 | isDebug := cfg.GetBool("DEBUG_DB", false) 44 | if isDebug { 45 | log.Infof("Connecting to database %s", connString) 46 | } 47 | 48 | db, err := gorm.Open(adapter, connString) 49 | if err != nil { 50 | return nil, errors.Wrap(err, "can't open db connection") 51 | } 52 | 53 | if isDebug { 54 | db = db.Debug() 55 | } 56 | 57 | db.SetLogger(logger{ 58 | log: log, 59 | }) 60 | 61 | return db, nil 62 | } 63 | 64 | func GetSQLDB(cfg config.Config, connString string) (*sql.DB, error) { 65 | adapter := strings.Split(connString, "://")[0] 66 | 67 | db, err := sql.Open(adapter, connString) 68 | if err != nil { 69 | return nil, errors.Wrap(err, "can't open db connection") 70 | } 71 | 72 | return db, nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/shared/db/gormdb/sql.go: -------------------------------------------------------------------------------- 1 | package gormdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | type sqlDBWithContext struct { 11 | underlying *sql.DB 12 | ctx context.Context 13 | } 14 | 15 | func (db *sqlDBWithContext) Exec(query string, args ...interface{}) (sql.Result, error) { 16 | return db.underlying.ExecContext(db.ctx, query, args...) 17 | } 18 | 19 | func (db *sqlDBWithContext) Prepare(query string) (*sql.Stmt, error) { 20 | return db.underlying.PrepareContext(db.ctx, query) 21 | } 22 | 23 | func (db *sqlDBWithContext) Query(query string, args ...interface{}) (*sql.Rows, error) { 24 | return db.underlying.QueryContext(db.ctx, query, args...) 25 | } 26 | 27 | func (db *sqlDBWithContext) QueryRow(query string, args ...interface{}) *sql.Row { 28 | return db.underlying.QueryRowContext(db.ctx, query, args...) 29 | } 30 | 31 | func (db *sqlDBWithContext) Begin() (*sql.Tx, error) { 32 | return db.underlying.BeginTx(db.ctx, nil) 33 | } 34 | 35 | func (db *sqlDBWithContext) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { 36 | return db.underlying.BeginTx(ctx, opts) 37 | } 38 | 39 | func FromSQL(ctx context.Context, db *sql.DB) (*gorm.DB, error) { 40 | return gorm.Open("postgres", &sqlDBWithContext{ // TODO 41 | underlying: db, 42 | ctx: ctx, 43 | }) 44 | } 45 | 46 | func FromTx(ctx context.Context, tx *sql.Tx) (*gorm.DB, error) { 47 | return gorm.Open("postgres", tx) 48 | } 49 | -------------------------------------------------------------------------------- /internal/shared/db/gormdb/tx.go: -------------------------------------------------------------------------------- 1 | package gormdb 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type FinishTxFunc func(err *error) 9 | 10 | func StartTx(db *gorm.DB) (*gorm.DB, FinishTxFunc, error) { 11 | tx := db.Begin() 12 | if tx.Error != nil { 13 | return nil, nil, errors.Wrap(tx.Error, "failed to start gorm transaction") 14 | } 15 | 16 | return tx, func(err *error) { 17 | finishTx(tx, err, recover()) 18 | }, nil 19 | } 20 | 21 | func finishTx(tx *gorm.DB, err *error, rec interface{}) { 22 | if rec != nil { 23 | if rollbackErr := tx.Rollback().Error; rollbackErr != nil { 24 | *err = errors.Wrapf(rollbackErr, "failed to rollback transaction after panic: %s", rec) 25 | } 26 | return 27 | } 28 | 29 | if *err != nil { 30 | if rollbackErr := tx.Rollback().Error; rollbackErr != nil { 31 | *err = errors.Wrapf(*err, "failed to rollback transaction: %s", rollbackErr) 32 | } 33 | return 34 | } 35 | 36 | if commitErr := tx.Commit().Error; commitErr != nil { 37 | *err = errors.Wrap(commitErr, "failed to commit transaction") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/shared/db/migrations/runner.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | "github.com/mattes/migrate" 8 | _ "github.com/mattes/migrate/source/file" // must have for migrations 9 | "github.com/pkg/errors" 10 | redsync "gopkg.in/redsync.v1" 11 | ) 12 | 13 | type Runner struct { 14 | distLock *redsync.Mutex 15 | log logutil.Log 16 | dbConnString string 17 | migrationsPath string 18 | } 19 | 20 | func NewRunner(distLock *redsync.Mutex, log logutil.Log, dbConnString, migrationsPath string) *Runner { 21 | return &Runner{ 22 | distLock: distLock, 23 | log: log, 24 | dbConnString: dbConnString, 25 | migrationsPath: migrationsPath, 26 | } 27 | } 28 | 29 | func (r Runner) Run() error { 30 | if err := r.distLock.Lock(); err != nil { 31 | // distLock waits until lock will be freed 32 | return errors.Wrap(err, "can't acquire dist lock") 33 | } 34 | defer r.distLock.Unlock() 35 | 36 | migrationsDir := fmt.Sprintf("file://%s", r.migrationsPath) 37 | m, err := migrate.New(migrationsDir, r.dbConnString) 38 | if err != nil { 39 | return errors.Wrap(err, "can't initialize migrations") 40 | } 41 | 42 | if err = m.Up(); err != nil { 43 | if err == migrate.ErrNoChange { 44 | r.log.Infof("Migrate: no ready to run migrations") 45 | return nil 46 | } 47 | 48 | return errors.Wrapf(err, "can't execute migrations in dir %s", r.migrationsPath) 49 | } 50 | 51 | r.log.Infof("Successfully executed database migrations") 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/shared/db/redis/pool.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/garyburd/redigo/redis" 9 | "github.com/golangci/golangci-api/internal/shared/config" 10 | ) 11 | 12 | func GetPool(cfg config.Config) (*redis.Pool, error) { 13 | redisURL, err := GetURL(cfg) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return &redis.Pool{ 19 | MaxIdle: 10, 20 | IdleTimeout: 240 * time.Second, 21 | TestOnBorrow: func(c redis.Conn, _ time.Time) error { 22 | _, pingErr := c.Do("PING") 23 | return pingErr 24 | }, 25 | Dial: func() (redis.Conn, error) { 26 | return redis.DialURL(redisURL) 27 | }, 28 | }, nil 29 | } 30 | 31 | func GetURL(cfg config.Config) (string, error) { 32 | if redisURL := cfg.GetString("REDIS_URL"); redisURL != "" { 33 | return redisURL, nil 34 | } 35 | 36 | host := cfg.GetString("REDIS_HOST") 37 | password := cfg.GetString("REDIS_PASSWORD") 38 | if host == "" || password == "" { 39 | return "", errors.New("no REDIS_URL or REDIS_{HOST,PASSWORD} in config") 40 | } 41 | 42 | return fmt.Sprintf("redis://h:%s@%s", password, host), nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/shared/fsutil/wd.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func GetProjectRoot() string { 9 | if os.Getenv("GO_ENV") == "prod" { 10 | return "./" // we are in heroku 11 | } 12 | 13 | // when we run "go test" current working dir changed to /test dir 14 | // so we need to restore root dir 15 | gopath := os.Getenv("GOPATH") 16 | return filepath.Join(gopath, "src", "github.com", "golangci", "golangci-api") 17 | } 18 | -------------------------------------------------------------------------------- /internal/shared/logutil/context.go: -------------------------------------------------------------------------------- 1 | package logutil 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | type Context map[string]interface{} 11 | 12 | func WrapLogWithContext(log Log, lctx Context) Log { 13 | return contextLog{ 14 | lctx: lctx, 15 | log: log, 16 | } 17 | } 18 | 19 | type contextLog struct { 20 | lctx Context 21 | log Log 22 | } 23 | 24 | func (cl contextLog) wrapFormat(format string) string { 25 | var pairs []string 26 | for k, v := range cl.lctx { 27 | pairs = append(pairs, fmt.Sprintf("%s=%v", color.YellowString(k), v)) 28 | } 29 | 30 | ctx := strings.Join(pairs, " ") 31 | if ctx != "" { 32 | ctx = "[" + ctx + "]" 33 | } 34 | return fmt.Sprintf("%s %s", format, ctx) 35 | } 36 | 37 | func (cl contextLog) Fatalf(format string, args ...interface{}) { 38 | cl.log.Fatalf(cl.wrapFormat(format), args...) 39 | } 40 | 41 | func (cl contextLog) Errorf(format string, args ...interface{}) { 42 | cl.log.Errorf(cl.wrapFormat(format), args...) 43 | } 44 | 45 | func (cl contextLog) Warnf(format string, args ...interface{}) { 46 | cl.log.Warnf(cl.wrapFormat(format), args...) 47 | } 48 | 49 | func (cl contextLog) Infof(format string, args ...interface{}) { 50 | cl.log.Infof(cl.wrapFormat(format), args...) 51 | } 52 | 53 | func (cl contextLog) Debugf(key string, format string, args ...interface{}) { 54 | cl.log.Debugf(key, cl.wrapFormat(format), args...) 55 | } 56 | 57 | func (cl contextLog) Child(name string) Log { 58 | return cl.log.Child(name) 59 | } 60 | 61 | func (cl contextLog) SetLevel(level LogLevel) { 62 | cl.log.SetLevel(level) 63 | } 64 | -------------------------------------------------------------------------------- /internal/shared/logutil/log.go: -------------------------------------------------------------------------------- 1 | package logutil 2 | 3 | type Func func(format string, args ...interface{}) 4 | 5 | type Log interface { 6 | Fatalf(format string, args ...interface{}) 7 | Errorf(format string, args ...interface{}) 8 | Warnf(format string, args ...interface{}) 9 | Infof(format string, args ...interface{}) 10 | Debugf(key string, format string, args ...interface{}) 11 | 12 | Child(name string) Log 13 | SetLevel(level LogLevel) 14 | } 15 | 16 | type LogLevel int 17 | 18 | const ( 19 | // debug message, write to debug logs only by logutils.Debug 20 | LogLevelDebug LogLevel = 0 21 | 22 | // information messages, don't write too much messages, 23 | // only useful ones: they are shown when running with -v 24 | LogLevelInfo LogLevel = 1 25 | 26 | // hidden errors: non critical errors: work can be continued, no need to fail whole program; 27 | // tests will crash if any warning occurred. 28 | LogLevelWarn LogLevel = 2 29 | 30 | // only not hidden from user errors: whole program failing, usually 31 | // error logging happens in 1-2 places: in the "main" function. 32 | LogLevelError LogLevel = 3 33 | ) 34 | -------------------------------------------------------------------------------- /internal/shared/providers/factory.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/golangci/golangci-api/internal/shared/logutil" 8 | "github.com/golangci/golangci-api/internal/shared/providers/implementations" 9 | "github.com/golangci/golangci-api/internal/shared/providers/provider" 10 | "github.com/golangci/golangci-api/pkg/api/models" 11 | "github.com/jinzhu/gorm" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type Factory interface { 16 | Build(auth *models.Auth) (provider.Provider, error) 17 | BuildForUser(db *gorm.DB, userID uint) (provider.Provider, error) 18 | BuildForToken(providerName, accessToken string) (provider.Provider, error) 19 | } 20 | 21 | type BasicFactory struct { 22 | log logutil.Log 23 | } 24 | 25 | func NewBasicFactory(log logutil.Log) *BasicFactory { 26 | return &BasicFactory{ 27 | log: log, 28 | } 29 | } 30 | 31 | func (f BasicFactory) BuildForToken(providerName, accessToken string) (provider.Provider, error) { 32 | switch providerName { 33 | case implementations.GithubProviderName: 34 | return implementations.NewGithub(f.log, accessToken), nil 35 | } 36 | 37 | return nil, fmt.Errorf("invalid provider name %q", providerName) 38 | } 39 | 40 | func (f BasicFactory) buildImpl(auth *models.Auth) (provider.Provider, error) { 41 | at := auth.AccessToken 42 | if auth.PrivateAccessToken != "" { 43 | at = auth.PrivateAccessToken 44 | } 45 | 46 | return f.BuildForToken(auth.Provider, at) 47 | } 48 | 49 | func (f BasicFactory) Build(auth *models.Auth) (provider.Provider, error) { 50 | p, err := f.buildImpl(auth) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return implementations.NewStableProvider(p, time.Second*30, 3), nil 56 | } 57 | 58 | func (f BasicFactory) BuildForUser(db *gorm.DB, userID uint) (provider.Provider, error) { 59 | var auth models.Auth 60 | if err := models.NewAuthQuerySet(db).UserIDEq(userID).One(&auth); err != nil { 61 | return nil, errors.Wrapf(err, "failed to get auth for user id %d", userID) 62 | } 63 | 64 | return f.Build(&auth) 65 | } 66 | -------------------------------------------------------------------------------- /internal/shared/providers/provider/errors.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | ErrUnauthorized = errors.New("no VCS provider authorization") 7 | ErrRepoWasArchived = errors.New("repo was archived so is read-only") 8 | ErrNoFreeOrgSeats = errors.New("no free seats in GitHub organization") // TODO: remove github 9 | ErrNotFound = errors.New("not found in VCS provider") 10 | ) 11 | 12 | func IsPermanentError(err error) bool { 13 | causeErr := errors.Cause(err) 14 | return causeErr == ErrRepoWasArchived || causeErr == ErrNotFound || 15 | causeErr == ErrUnauthorized || causeErr == ErrNoFreeOrgSeats 16 | } 17 | -------------------------------------------------------------------------------- /internal/shared/providers/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/pkg/api/models" 7 | ) 8 | 9 | type Provider interface { 10 | Name() string 11 | 12 | LinkToPullRequest(repo *models.Repo, num int) string 13 | 14 | SetBaseURL(url string) error 15 | 16 | GetBranch(ctx context.Context, owner, repo, branch string) (*Branch, error) 17 | GetRepoByName(ctx context.Context, owner, repo string) (*Repo, error) 18 | GetPullRequest(ctx context.Context, owner, repo string, number int) (*PullRequest, error) 19 | 20 | GetOrgMembershipByName(ctx context.Context, org string) (*OrgMembership, error) 21 | 22 | ListRepoHooks(ctx context.Context, owner, repo string) ([]Hook, error) 23 | CreateRepoHook(ctx context.Context, owner, repo string, hook *HookConfig) (*Hook, error) 24 | DeleteRepoHook(ctx context.Context, owner, repo string, hookID int) error 25 | 26 | ListRepos(ctx context.Context, cfg *ListReposConfig) ([]Repo, error) 27 | ListOrgMemberships(ctx context.Context, cfg *ListOrgsConfig) ([]OrgMembership, error) 28 | 29 | ListPullRequestCommits(ctx context.Context, owner, repo string, number int) ([]*Commit, error) 30 | SetCommitStatus(ctx context.Context, owner, repo, ref string, status *CommitStatus) error 31 | 32 | ParsePullRequestEvent(ctx context.Context, payload []byte) (*PullRequestEvent, error) 33 | 34 | AddCollaborator(ctx context.Context, owner, repo, username string) (*RepoInvitation, error) 35 | RemoveCollaborator(ctx context.Context, owner, repo, username string) error 36 | AcceptRepoInvitation(ctx context.Context, invitationID int) error 37 | } 38 | -------------------------------------------------------------------------------- /internal/shared/queue/consumers/consumer.go: -------------------------------------------------------------------------------- 1 | package consumers 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type ResultLogger func(err error) 8 | 9 | type Consumer interface { 10 | ConsumeMessage(ctx context.Context, message []byte) error 11 | ResultLogger() ResultLogger 12 | } 13 | -------------------------------------------------------------------------------- /internal/shared/queue/consumers/errors.go: -------------------------------------------------------------------------------- 1 | package consumers 2 | 3 | import "errors" 4 | 5 | var ErrRetryLater = errors.New("retry later") 6 | var ErrPermanent = errors.New("permanent error") 7 | var ErrBadMessage = errors.New("bad message") 8 | -------------------------------------------------------------------------------- /internal/shared/queue/consumers/multiplexer.go: -------------------------------------------------------------------------------- 1 | package consumers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type Multiplexer struct { 12 | consumers map[string]Consumer 13 | resultLogger ResultLogger 14 | } 15 | 16 | func NewMultiplexer() *Multiplexer { 17 | return &Multiplexer{ 18 | consumers: map[string]Consumer{}, 19 | } 20 | } 21 | 22 | func (m *Multiplexer) SetResultLogger(logger ResultLogger) { 23 | m.resultLogger = logger 24 | } 25 | 26 | func (m Multiplexer) ResultLogger() ResultLogger { 27 | return m.resultLogger 28 | } 29 | 30 | type subconsumerMessage struct { 31 | SubqueueID string 32 | Message json.RawMessage 33 | } 34 | 35 | func (m Multiplexer) consumerNames() []string { 36 | var ret []string 37 | for name := range m.consumers { 38 | ret = append(ret, name) 39 | } 40 | 41 | return ret 42 | } 43 | 44 | func (m *Multiplexer) ConsumeMessage(ctx context.Context, message []byte) error { 45 | var sm subconsumerMessage 46 | if err := json.Unmarshal(message, &sm); err != nil { 47 | return errors.Wrap(err, "json unmarshal failed") 48 | } 49 | 50 | consumer := m.consumers[sm.SubqueueID] 51 | if consumer == nil { 52 | return fmt.Errorf("no consumer with id %s, registered consumers: %v", sm.SubqueueID, m.consumerNames()) 53 | } 54 | 55 | return consumer.ConsumeMessage(ctx, []byte(sm.Message)) 56 | } 57 | 58 | func (m *Multiplexer) RegisterConsumer(id string, consumer Consumer) error { 59 | if m.consumers[id] != nil { 60 | return fmt.Errorf("consumer %s is already registered", id) 61 | } 62 | m.consumers[id] = consumer 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/shared/queue/message.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | type Message interface { 4 | LockID() string 5 | } 6 | -------------------------------------------------------------------------------- /internal/shared/queue/producers/base.go: -------------------------------------------------------------------------------- 1 | package producers 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/queue" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type Base struct { 9 | q Queue 10 | } 11 | 12 | func (p *Base) Register(m *Multiplexer, queueID string) error { 13 | q, err := m.NewSubqueue(queueID) 14 | if err != nil { 15 | return errors.Wrapf(err, "failed to create %s subqueue", queueID) 16 | } 17 | 18 | p.q = q 19 | return nil 20 | } 21 | 22 | func (p *Base) Put(message queue.Message) error { 23 | return p.q.Put(message) 24 | } 25 | -------------------------------------------------------------------------------- /internal/shared/queue/producers/multiplexer.go: -------------------------------------------------------------------------------- 1 | package producers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/queue" 7 | ) 8 | 9 | type Multiplexer struct { 10 | q Queue 11 | subqueues map[string]bool 12 | } 13 | 14 | func NewMultiplexer(q Queue) *Multiplexer { 15 | return &Multiplexer{ 16 | q: q, 17 | subqueues: map[string]bool{}, 18 | } 19 | } 20 | 21 | type subqueue struct { 22 | id string 23 | parent *Multiplexer 24 | } 25 | 26 | type subqueueMessage struct { 27 | SubqueueID string 28 | Message queue.Message 29 | } 30 | 31 | func (sm subqueueMessage) LockID() string { 32 | return sm.Message.LockID() 33 | } 34 | 35 | func (sq subqueue) Put(message queue.Message) error { 36 | return sq.parent.q.Put(subqueueMessage{ 37 | SubqueueID: sq.id, 38 | Message: message, 39 | }) 40 | } 41 | 42 | func (m *Multiplexer) NewSubqueue(id string) (Queue, error) { 43 | if m.subqueues[id] { 44 | return nil, fmt.Errorf("subqueue %s is already registered", id) 45 | } 46 | m.subqueues[id] = true 47 | 48 | return &subqueue{ 49 | id: id, 50 | parent: m, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/shared/queue/producers/queue.go: -------------------------------------------------------------------------------- 1 | package producers 2 | 3 | import "github.com/golangci/golangci-api/internal/shared/queue" 4 | 5 | type Queue interface { 6 | Put(message queue.Message) error 7 | } 8 | -------------------------------------------------------------------------------- /migrations/10_add_github_hook_id_to_github_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | DROP COLUMN github_hook_id; 3 | -------------------------------------------------------------------------------- /migrations/10_add_github_hook_id_to_github_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | ADD COLUMN github_hook_id INTEGER NOT NULL; 3 | -------------------------------------------------------------------------------- /migrations/11_create_github_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE github_analyzes; 2 | -------------------------------------------------------------------------------- /migrations/11_create_github_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE github_analyzes ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | github_repo_id INTEGER REFERENCES github_repos(id) NOT NULL, 8 | github_pull_request_number INTEGER NOT NULL, 9 | github_delivery_guid VARCHAR(64) NOT NULL UNIQUE, 10 | 11 | status VARCHAR(64) NOT NULL 12 | ); 13 | -------------------------------------------------------------------------------- /migrations/12_add_reported_issues_count_to_github_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes 2 | DROP COLUMN reported_issues_count; 3 | -------------------------------------------------------------------------------- /migrations/12_add_reported_issues_count_to_github_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes 2 | ADD COLUMN reported_issues_count INTEGER NOT NULL DEFAULT -1; 3 | -------------------------------------------------------------------------------- /migrations/13_add_commit_sha_to_github_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes 2 | DROP COLUMN commit_sha; 3 | 4 | ALTER TABLE github_analyzes 5 | DROP INDEX github_analyzes_uniq_repo_and_commit_sha; 6 | -------------------------------------------------------------------------------- /migrations/13_add_commit_sha_to_github_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes 2 | ADD COLUMN commit_sha VARCHAR(64) NOT NULL DEFAULT ''; 3 | 4 | CREATE UNIQUE INDEX github_analyzes_uniq_repo_and_commit_sha 5 | ON github_analyzes(github_repo_id, commit_sha) 6 | WHERE commit_sha != ''; 7 | -------------------------------------------------------------------------------- /migrations/14_add_private_access_token_to_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths 2 | DROP COLUMN private_access_token; 3 | -------------------------------------------------------------------------------- /migrations/14_add_private_access_token_to_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths 2 | ADD COLUMN private_access_token VARCHAR(128); 3 | -------------------------------------------------------------------------------- /migrations/15_drop_commit_sha_and_repo_uniq_index_from_github_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX github_analyzes_uniq_repo_and_commit_sha 2 | ON github_analyzes(github_repo_id, commit_sha) 3 | WHERE commit_sha != ''; 4 | -------------------------------------------------------------------------------- /migrations/15_drop_commit_sha_and_repo_uniq_index_from_github_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX github_analyzes_uniq_repo_and_commit_sha; 2 | -------------------------------------------------------------------------------- /migrations/16_add_result_json_to_github_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes 2 | DROP COLUMN result_json; -------------------------------------------------------------------------------- /migrations/16_add_result_json_to_github_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes 2 | ADD COLUMN result_json JSONB; -------------------------------------------------------------------------------- /migrations/17_add_index_on_github_pull_request_number_to_github_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX github_pull_request_number_idx; 2 | -------------------------------------------------------------------------------- /migrations/17_add_index_on_github_pull_request_number_to_github_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX github_pull_request_number_idx ON github_analyzes (github_pull_request_number); 2 | -------------------------------------------------------------------------------- /migrations/18_create_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE repo_analysis_statuses; 2 | -------------------------------------------------------------------------------- /migrations/18_create_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE repo_analysis_statuses ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | name VARCHAR(256) NOT NULL UNIQUE, 8 | last_analyzed_at TIMESTAMP, 9 | version INTEGER NOT NULL 10 | ); 11 | -------------------------------------------------------------------------------- /migrations/19_create_repo_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE repo_analyzes; -------------------------------------------------------------------------------- /migrations/19_create_repo_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE repo_analyzes ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | repo_analysis_status_id INTEGER REFERENCES repo_analysis_statuses(id) NOT NULL, 8 | analysis_guid VARCHAR(64) NOT NULL UNIQUE, 9 | status VARCHAR(64) NOT NULL, 10 | 11 | result_json JSONB 12 | ); 13 | -------------------------------------------------------------------------------- /migrations/1_add_users.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; 2 | -------------------------------------------------------------------------------- /migrations/1_add_users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | email VARCHAR(128) NOT NULL, 8 | nickname VARCHAR(128) NOT NULL, 9 | name VARCHAR(128) 10 | ); 11 | -------------------------------------------------------------------------------- /migrations/20_add_fields_to_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | DROP COLUMN has_pending_changes; -------------------------------------------------------------------------------- /migrations/20_add_fields_to_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ADD COLUMN has_pending_changes boolean; -------------------------------------------------------------------------------- /migrations/21_add_default_branch_to_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | DROP COLUMN default_branch; -------------------------------------------------------------------------------- /migrations/21_add_default_branch_to_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ADD COLUMN default_branch VARCHAR(64); -------------------------------------------------------------------------------- /migrations/22_add_commit_sha_to_repo_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analyzes 2 | DROP COLUMN commit_sha 3 | DROP INDEX repo_analyzes_uniq_status_id_and_commit_sha; 4 | 5 | ALTER TABLE repo_analysis_statuses 6 | DROP COLUMN pending_commit_sha -------------------------------------------------------------------------------- /migrations/22_add_commit_sha_to_repo_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analyzes 2 | ADD COLUMN commit_sha VARCHAR(64) NOT NULL DEFAULT ''; 3 | 4 | ALTER TABLE repo_analysis_statuses 5 | ADD COLUMN pending_commit_sha VARCHAR(64) NOT NULL DEFAULT ''; 6 | 7 | CREATE UNIQUE INDEX repo_analyzes_uniq_status_id_and_commit_sha 8 | ON repo_analyzes(repo_analysis_status_id, commit_sha) 9 | WHERE commit_sha != ''; -------------------------------------------------------------------------------- /migrations/23_add_index_on_status_to_repo_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX repo_analyzes_status_idx; -------------------------------------------------------------------------------- /migrations/23_add_index_on_status_to_repo_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX repo_analyzes_status_idx ON repo_analyzes(status); -------------------------------------------------------------------------------- /migrations/24_add_attempt_number_repo_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analyzes 2 | DROP COLUMN attempt_number; -------------------------------------------------------------------------------- /migrations/24_add_attempt_number_repo_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analyzes 2 | ADD COLUMN attempt_number INTEGER NOT NULL DEFAULT 1; -------------------------------------------------------------------------------- /migrations/25_add_linters_version_to_repo_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analyzes 2 | DROP COLUMN linters_version; 3 | 4 | CREATE UNIQUE INDEX repo_analyzes_uniq_status_id_and_commit_sha 5 | ON repo_analyzes(repo_analysis_status_id, commit_sha) 6 | WHERE commit_sha != ''; 7 | 8 | DROP INDEX repo_analyzes_uniq_status_id_and_commit_sha_and_linters_version; 9 | DROP INDEX repo_analyzes_linters_version_idx; 10 | DROP INDEX repo_analysis_statuses_has_pending_changes_idx; -------------------------------------------------------------------------------- /migrations/25_add_linters_version_to_repo_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analyzes 2 | ADD COLUMN linters_version VARCHAR(64) NOT NULL DEFAULT 'v1.10'; 3 | 4 | DROP INDEX repo_analyzes_uniq_status_id_and_commit_sha; 5 | 6 | CREATE UNIQUE INDEX repo_analyzes_uniq_status_id_and_commit_sha_and_linters_version 7 | ON repo_analyzes(repo_analysis_status_id, commit_sha, linters_version); 8 | 9 | CREATE INDEX repo_analyzes_linters_version_idx 10 | ON repo_analyzes(linters_version); 11 | 12 | CREATE INDEX repo_analysis_statuses_has_pending_changes_idx 13 | ON repo_analysis_statuses(has_pending_changes); -------------------------------------------------------------------------------- /migrations/26_add_last_analyzed_linters_version_to_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | DROP COLUMN last_analyzed_linters_version; 3 | 4 | DROP INDEX repo_analysis_statuses_last_analyzed_linters_version_idx; -------------------------------------------------------------------------------- /migrations/26_add_last_analyzed_linters_version_to_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ADD COLUMN last_analyzed_linters_version VARCHAR(64); 3 | 4 | CREATE INDEX repo_analysis_statuses_last_analyzed_linters_version_idx 5 | ON repo_analysis_statuses(last_analyzed_linters_version); -------------------------------------------------------------------------------- /migrations/27_add_active_to_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | DROP COLUMN active; -------------------------------------------------------------------------------- /migrations/27_add_active_to_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ADD COLUMN active BOOLEAN NOT NULL DEFAULT true; -------------------------------------------------------------------------------- /migrations/28_add_display_name_to_github_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | DROP COLUMN display_name; -------------------------------------------------------------------------------- /migrations/28_add_display_name_to_github_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | ADD COLUMN display_name VARCHAR(256) NOT NULL DEFAULT ''; -------------------------------------------------------------------------------- /migrations/29_lowercase_names_in_github_repos.down.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golangci/golangci-api/081ecb458d4aebd2b8e390cd2de9aadf83d48f8e/migrations/29_lowercase_names_in_github_repos.down.sql -------------------------------------------------------------------------------- /migrations/29_lowercase_names_in_github_repos.up.sql: -------------------------------------------------------------------------------- 1 | UPDATE github_repos 2 | SET display_name = name, name = lower(name) WHERE display_name = ''; 3 | 4 | -------------------------------------------------------------------------------- /migrations/2_add_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE github_auths; 2 | -------------------------------------------------------------------------------- /migrations/2_add_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE github_auths ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | access_token VARCHAR(128) NOT NULL, 8 | raw_data TEXT NOT NULL, 9 | user_id INTEGER REFERENCES users(id) NOT NULL 10 | ); 11 | -------------------------------------------------------------------------------- /migrations/30_add_github_user_id_to_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths 2 | DROP COLUMN github_user_id; 3 | -------------------------------------------------------------------------------- /migrations/30_add_github_user_id_to_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths 2 | ADD COLUMN github_user_id BIGINT NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /migrations/31_add_index_on_github_user_id_to_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX github_auths_github_user_id_idx; -------------------------------------------------------------------------------- /migrations/31_add_index_on_github_user_id_to_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX github_auths_github_user_id_idx ON github_auths(github_user_id) 2 | WHERE github_user_id != 0; -------------------------------------------------------------------------------- /migrations/32_drop_uniq_login_index_from_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths ADD CONSTRAINT uniq_login UNIQUE (login); -------------------------------------------------------------------------------- /migrations/32_drop_uniq_login_index_from_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths DROP CONSTRAINT uniq_login; -------------------------------------------------------------------------------- /migrations/33_add_github_id_to_github_repos.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX github_repos_github_id_idx; 2 | 3 | ALTER TABLE github_repos 4 | DROP COLUMN github_id; -------------------------------------------------------------------------------- /migrations/33_add_github_id_to_github_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | ADD COLUMN github_id INTEGER NOT NULL DEFAULT 0; 3 | 4 | CREATE UNIQUE INDEX github_repos_github_id_idx ON github_repos(github_id) 5 | WHERE github_id != 0 AND deleted_at IS NULL; -------------------------------------------------------------------------------- /migrations/34_rename_github_repos_to_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos RENAME TO github_repos; 2 | ALTER SEQUENCE repos_id_seq RENAME TO github_repos_id_seq; -------------------------------------------------------------------------------- /migrations/34_rename_github_repos_to_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos RENAME TO repos; 2 | ALTER SEQUENCE github_repos_id_seq RENAME TO repos_id_seq; -------------------------------------------------------------------------------- /migrations/35_rename_github_analyzes_to_pull_request_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pull_request_analyzes RENAME TO github_analyzes; 2 | ALTER SEQUENCE pull_request_analyzes_id_seq RENAME TO github_analyzes_id_seq; -------------------------------------------------------------------------------- /migrations/35_rename_github_analyzes_to_pull_request_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_analyzes RENAME TO pull_request_analyzes; 2 | ALTER SEQUENCE github_analyzes_id_seq RENAME TO pull_request_analyzes_id_seq; -------------------------------------------------------------------------------- /migrations/36_remove_github_from_some_columns_in_pull_request_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pull_request_analyzes RENAME repo_id to github_repo_id; 2 | ALTER TABLE pull_request_analyzes RENAME pull_request_number to github_pull_request_number; -------------------------------------------------------------------------------- /migrations/36_remove_github_from_some_columns_in_pull_request_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pull_request_analyzes RENAME github_repo_id TO repo_id; 2 | ALTER TABLE pull_request_analyzes RENAME github_pull_request_number TO pull_request_number; -------------------------------------------------------------------------------- /migrations/37_add_provider_to_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos 2 | DROP COLUMN provider; -------------------------------------------------------------------------------- /migrations/37_add_provider_to_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos 2 | ADD COLUMN provider VARCHAR(64) NOT NULL DEFAULT 'github.com'; 3 | 4 | CREATE INDEX repos_provider_idx ON repos(provider); -------------------------------------------------------------------------------- /migrations/38_add_repo_id_to_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses DROP COLUMN repo_id; -------------------------------------------------------------------------------- /migrations/38_add_repo_id_to_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ADD COLUMN repo_id INTEGER REFERENCES repos(id); 3 | 4 | CREATE INDEX repo_analysis_statuses_repo_id_idx ON repo_analysis_statuses(repo_id); 5 | 6 | UPDATE repo_analysis_statuses SET repo_id=(SELECT id FROM repos WHERE name = repo_analysis_statuses.name LIMIT 1); -------------------------------------------------------------------------------- /migrations/39_add_index_on_repo_analysis_status_id_to_repo_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX repo_analyzes_repo_analysis_status_id_idx; -------------------------------------------------------------------------------- /migrations/39_add_index_on_repo_analysis_status_id_to_repo_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX repo_analyzes_repo_analysis_status_id_idx ON repo_analyzes(repo_analysis_status_id); -------------------------------------------------------------------------------- /migrations/3_add_index_on_nickname_to_users.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX nickname_idx; 2 | -------------------------------------------------------------------------------- /migrations/3_add_index_on_nickname_to_users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX nickname_idx ON users (nickname); 2 | -------------------------------------------------------------------------------- /migrations/40_make_repo_id_not_null_in_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ALTER COLUMN repo_id SET NULL; 3 | 4 | DROP INDEX repo_analysis_statuses_repo_id_idx; 5 | CREATE INDEX repo_analysis_statuses_repo_id_idx ON repo_analysis_statuses(repo_id); 6 | 7 | ALTER TABLE repo_analysis_statuses 8 | ADD COLUMN name VARCHAR(256); -------------------------------------------------------------------------------- /migrations/40_make_repo_id_not_null_in_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses 2 | ALTER COLUMN repo_id SET NOT NULL; 3 | 4 | DROP INDEX repo_analysis_statuses_repo_id_idx; 5 | CREATE UNIQUE INDEX repo_analysis_statuses_repo_id_idx ON repo_analysis_statuses(repo_id); 6 | 7 | ALTER TABLE repo_analysis_statuses 8 | DROP COLUMN name; -------------------------------------------------------------------------------- /migrations/41_rename_github_auths_to_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE auths RENAME TO github_auths; 2 | ALTER SEQUENCE auths_id_seq RENAME TO github_auths_id_seq; -------------------------------------------------------------------------------- /migrations/41_rename_github_auths_to_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths RENAME TO auths; 2 | ALTER SEQUENCE github_auths_id_seq RENAME TO auths_id_seq; -------------------------------------------------------------------------------- /migrations/42_rename_github_to_provider_in_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE auths RENAME provider_user_id TO github_user_id; 2 | ALTER TABLE auths DROP COLUMN provider; -------------------------------------------------------------------------------- /migrations/42_rename_github_to_provider_in_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE auths RENAME github_user_id TO provider_user_id; 2 | ALTER TABLE auths ADD COLUMN provider VARCHAR(64) NOT NULL DEFAULT 'github.com'; -------------------------------------------------------------------------------- /migrations/43_rename_github_to_provider_in_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos RENAME provider_id TO github_id; 2 | ALTER TABLE repos RENAME provider_hook_id TO github_hook_id; -------------------------------------------------------------------------------- /migrations/43_rename_github_to_provider_in_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos RENAME github_id TO provider_id; 2 | ALTER TABLE repos RENAME github_hook_id TO provider_hook_id; -------------------------------------------------------------------------------- /migrations/44_add_commit_state_to_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos DROP COLUMN commit_state; -------------------------------------------------------------------------------- /migrations/44_add_commit_state_to_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos ADD COLUMN commit_state VARCHAR(64) NOT NULL DEFAULT 'done'; -------------------------------------------------------------------------------- /migrations/45_add_stargazers_count_to_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos DROP COLUMN stargazers_count; -------------------------------------------------------------------------------- /migrations/45_add_stargazers_count_to_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos ADD COLUMN stargazers_count INT NOT NULL DEFAULT -1; -------------------------------------------------------------------------------- /migrations/46_add_orgs.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE orgs; 2 | -------------------------------------------------------------------------------- /migrations/46_add_orgs.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE orgs ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | name VARCHAR(128) NOT NULL, 8 | display_name VARCHAR(128) NOT NULL, 9 | 10 | provider VARCHAR(64) NOT NULL DEFAULT 'github.com', 11 | provider_id INTEGER NOT NULL DEFAULT 0, 12 | provider_personal_user_id INTEGER NOT NULL DEFAULT 0, 13 | 14 | settings JSON NOT NULL DEFAULT '{}' 15 | ); 16 | -------------------------------------------------------------------------------- /migrations/47_add_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE org_subs; -------------------------------------------------------------------------------- /migrations/47_add_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE org_subs ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | payment_gateway_card_token VARCHAR(64) NOT NULL, 8 | payment_gateway_customer_id VARCHAR(64), 9 | payment_gateway_subscription_id VARCHAR(64), 10 | 11 | billing_user_id INTEGER REFERENCES users(id) NOT NULL, 12 | org_id INTEGER REFERENCES orgs(id) NOT NULL, 13 | seats_count INTEGER NOT NULL DEFAULT 1, 14 | commit_state VARCHAR(32) NOT NULL 15 | ); 16 | -------------------------------------------------------------------------------- /migrations/48_add_idempotency_key_to_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs DROP COLUMN idempotency_key; -------------------------------------------------------------------------------- /migrations/48_add_idempotency_key_to_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs ADD COLUMN idempotency_key VARCHAR(64) UNIQUE; -------------------------------------------------------------------------------- /migrations/49_add_payment_gateway_events.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE payment_gateway_events; -------------------------------------------------------------------------------- /migrations/49_add_payment_gateway_events.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE payment_gateway_events ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | provider VARCHAR(64) NOT NULL DEFAULT 'securionpay', 8 | provider_id VARCHAR(64) NOT NULL DEFAULT '', 9 | 10 | type VARCHAR(32) NOT NULL DEFAULT '', 11 | data JSON NOT NULL DEFAULT '{}', 12 | 13 | UNIQUE(provider, provider_id) 14 | ); 15 | -------------------------------------------------------------------------------- /migrations/4_add_avatar_url_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN avatar_url; 2 | -------------------------------------------------------------------------------- /migrations/4_add_avatar_url_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN avatar_url varchar(256); 2 | -------------------------------------------------------------------------------- /migrations/50_add_user_id_to_payment_gateway_events.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE payment_gateway_events DROP COLUMN user_id; -------------------------------------------------------------------------------- /migrations/50_add_user_id_to_payment_gateway_events.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE payment_gateway_events 2 | ADD COLUMN user_id INTEGER REFERENCES users(id); 3 | 4 | CREATE INDEX payment_gateway_events_user_id_idx 5 | ON payment_gateway_events(user_id) 6 | WHERE user_id IS NOT NULL; -------------------------------------------------------------------------------- /migrations/51_add_index_to_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX org_subs_org_id_uniq_idx; 2 | DROP INDEX org_subs_org_id_idx; -------------------------------------------------------------------------------- /migrations/51_add_index_to_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX org_subs_org_id_uniq_idx 2 | ON org_subs(org_id) 3 | WHERE deleted_at IS NULL; 4 | 5 | CREATE INDEX org_subs_org_id_idx ON org_subs(org_id) -------------------------------------------------------------------------------- /migrations/52_add_payment_gateway_name_to_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs DROP COLUMN payment_gateway_name; -------------------------------------------------------------------------------- /migrations/52_add_payment_gateway_name_to_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs 2 | ADD COLUMN payment_gateway_name VARCHAR(64); -------------------------------------------------------------------------------- /migrations/53_add_version_to_org_and_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE orgs DROP COLUMN version; 2 | ALTER TABLE org_subs DROP COLUMN version; -------------------------------------------------------------------------------- /migrations/53_add_version_to_org_and_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE orgs 2 | ADD COLUMN version INTEGER NOT NULL DEFAULT 0; 3 | 4 | ALTER TABLE org_subs 5 | ADD COLUMN version INTEGER NOT NULL DEFAULT 0; -------------------------------------------------------------------------------- /migrations/54_add_price_per_seat_to_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs 2 | DROP COLUMN price_per_seat; -------------------------------------------------------------------------------- /migrations/54_add_price_per_seat_to_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs 2 | ADD COLUMN price_per_seat VARCHAR(64) NOT NULL; -------------------------------------------------------------------------------- /migrations/55_add_cancel_url_to_org_subs.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs DROP COLUMN cancel_url; -------------------------------------------------------------------------------- /migrations/55_add_cancel_url_to_org_subs.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE org_subs 2 | ADD COLUMN cancel_url TEXT NOT NULL; -------------------------------------------------------------------------------- /migrations/56_add_is_private_to_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos 2 | DROP COLUMN is_private; -------------------------------------------------------------------------------- /migrations/56_add_is_private_to_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos 2 | ADD COLUMN is_private BOOLEAN NOT NULL DEFAULT false; -------------------------------------------------------------------------------- /migrations/57_add_create_fail_reason_to_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos DROP COLUMN create_fail_reason; -------------------------------------------------------------------------------- /migrations/57_add_create_fail_reason_to_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repos ADD COLUMN create_fail_reason VARCHAR(128) NULL; -------------------------------------------------------------------------------- /migrations/58_add_is_empty_to_repo_analysis_statuses.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses DROP COLUMN is_empty; -------------------------------------------------------------------------------- /migrations/58_add_is_empty_to_repo_analysis_statuses.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE repo_analysis_statuses ADD COLUMN is_empty BOOLEAN NOT NULL DEFAULT false; -------------------------------------------------------------------------------- /migrations/59_add_index_to_pull_request_analyzes.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX pull_request_analyzes_commit_sha_idx; -------------------------------------------------------------------------------- /migrations/59_add_index_to_pull_request_analyzes.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX pull_request_analyzes_commit_sha_idx ON pull_request_analyzes(commit_sha); -------------------------------------------------------------------------------- /migrations/5_add_login_to_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths DROP COLUMN login; 2 | ALTER TABLE github_auths DROP CONSTRAINT uniq_user_id_login; 3 | -------------------------------------------------------------------------------- /migrations/5_add_login_to_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths ADD COLUMN login varchar(256) NOT NULL; 2 | ALTER TABLE github_auths ADD CONSTRAINT uniq_user_id_login UNIQUE (user_id, login); 3 | -------------------------------------------------------------------------------- /migrations/6_drop_nickname_from_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN nickname varchar(128) NOT NULL; 2 | -------------------------------------------------------------------------------- /migrations/6_drop_nickname_from_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN nickname; 2 | -------------------------------------------------------------------------------- /migrations/7_fix_uniqs_int_github_auths.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths ADD CONSTRAINT uniq_user_id_login UNIQUE (user_id, login); 2 | ALTER TABLE github_auths DROP CONSTRAINT uniq_user_id; 3 | ALTER TABLE github_auths DROP CONSTRAINT uniq_login; 4 | -------------------------------------------------------------------------------- /migrations/7_fix_uniqs_int_github_auths.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_auths DROP CONSTRAINT uniq_user_id_login; 2 | ALTER TABLE github_auths ADD CONSTRAINT uniq_user_id UNIQUE (user_id); 3 | ALTER TABLE github_auths ADD CONSTRAINT uniq_login UNIQUE (login); 4 | -------------------------------------------------------------------------------- /migrations/8_add_github_repos.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE github_repos; 2 | -------------------------------------------------------------------------------- /migrations/8_add_github_repos.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE github_repos ( 2 | id SERIAL PRIMARY KEY, 3 | created_at TIMESTAMP NOT NULL, 4 | updated_at TIMESTAMP NOT NULL, 5 | deleted_at TIMESTAMP, 6 | 7 | user_id INTEGER REFERENCES users(id) NOT NULL, 8 | name VARCHAR(256) NOT NULL 9 | ); 10 | 11 | CREATE UNIQUE INDEX github_repos_uniq_name 12 | ON github_repos(name) 13 | WHERE deleted_at IS NULL; 14 | -------------------------------------------------------------------------------- /migrations/9_add_hook_id_to_github_repos.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | DROP COLUMN hook_id; 3 | -------------------------------------------------------------------------------- /migrations/9_add_hook_id_to_github_repos.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE github_repos 2 | ADD COLUMN hook_id varchar(32) NOT NULL UNIQUE; 3 | -------------------------------------------------------------------------------- /pkg/api/app_modifiers.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/providers" 5 | ) 6 | 7 | type Modifier func(a *App) 8 | 9 | func SetProviderFactory(pf providers.Factory) Modifier { 10 | return func(a *App) { 11 | a.providerFactory = pf 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/api/auth/authorizer.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/api/apierrors" 5 | "github.com/golangci/golangci-api/internal/api/session" 6 | "github.com/golangci/golangci-api/pkg/api/models" 7 | "github.com/jinzhu/gorm" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const userIDSessKey = "UserID" 12 | const sessType = "s" 13 | 14 | type Authorizer struct { 15 | db *gorm.DB 16 | asf *session.Factory 17 | } 18 | 19 | func NewAuthorizer(db *gorm.DB, asf *session.Factory) *Authorizer { 20 | return &Authorizer{ 21 | db: db, 22 | asf: asf, 23 | } 24 | } 25 | 26 | type AuthenticatedUser struct { 27 | Auth *models.Auth 28 | AuthSess *session.Session 29 | 30 | User *models.User 31 | } 32 | 33 | func (a Authorizer) Authorize(sctx *session.RequestContext) (*AuthenticatedUser, error) { 34 | authSess, err := a.asf.Build(sctx, sessType) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "failed to build auth sess") 37 | } 38 | 39 | authModel, err := a.getAuthFromSession(authSess) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | var user models.User 45 | if err := models.NewUserQuerySet(a.db).IDEq(authModel.UserID).One(&user); err != nil { 46 | return nil, errors.Wrapf(err, "failed to fetch user %d from db", authModel.UserID) 47 | } 48 | 49 | return &AuthenticatedUser{ 50 | Auth: authModel, 51 | AuthSess: authSess, 52 | User: &user, 53 | }, nil 54 | } 55 | 56 | func (a Authorizer) getAuthFromSession(authSess *session.Session) (*models.Auth, error) { 57 | userIDobj := authSess.GetValue(userIDSessKey) 58 | if userIDobj == nil { 59 | return nil, apierrors.ErrNotAuthorized 60 | } 61 | 62 | userIDfloat := userIDobj.(float64) 63 | userID := uint(userIDfloat) 64 | 65 | var auth models.Auth 66 | if err := models.NewAuthQuerySet(a.db).UserIDEq(userID).One(&auth); err != nil { 67 | if err == gorm.ErrRecordNotFound { 68 | return nil, errors.Wrapf(err, "no user with id %d", userID) 69 | } 70 | 71 | return nil, errors.Wrapf(err, "failed to fetch user with id %d", userID) 72 | } 73 | 74 | return &auth, nil 75 | } 76 | 77 | func (a Authorizer) CreateAuthorization(sctx *session.RequestContext, user *models.User) error { 78 | authSess, err := a.asf.Build(sctx, sessType) 79 | if err != nil { 80 | return errors.Wrap(err, "failed to build auth sess") 81 | } 82 | 83 | authSess.Set(userIDSessKey, user.ID) 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/api/auth/oauth/factory.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/golangci/golangci-api/internal/api/session" 7 | "github.com/golangci/golangci-api/internal/shared/config" 8 | "github.com/golangci/golangci-api/internal/shared/logutil" 9 | "github.com/markbates/goth/providers/github" 10 | ) 11 | 12 | type Factory struct { 13 | sessFactory *session.Factory 14 | log logutil.Log 15 | cfg config.Config 16 | } 17 | 18 | func NewFactory(sessFactory *session.Factory, log logutil.Log, cfg config.Config) *Factory { 19 | return &Factory{ 20 | sessFactory: sessFactory, 21 | log: log, 22 | cfg: cfg, 23 | } 24 | } 25 | 26 | func (f Factory) BuildAuthorizer(providerName string, isPrivate bool) (*Authorizer, error) { 27 | if providerName != "github" { 28 | return nil, fmt.Errorf("provider %s isn't support for OAuth", providerName) 29 | } 30 | 31 | cbURL := fmt.Sprintf("/v1/auth/%s/callback/", providerName) 32 | if isPrivate { 33 | cbURL += "private" 34 | providerName += "_private" 35 | } else { 36 | cbURL += "public" 37 | } 38 | 39 | key := f.cfg.GetString("GITHUB_KEY") 40 | secret := f.cfg.GetString("GITHUB_SECRET") 41 | cbHost := f.cfg.GetString("GITHUB_CALLBACK_HOST") 42 | 43 | if key == "" || secret == "" || cbHost == "" { 44 | return nil, fmt.Errorf("not all required GITHUB_* config params are set") 45 | } 46 | 47 | var scopes []string 48 | 49 | if isPrivate { 50 | scopes = []string{ 51 | "user:email", 52 | "repo", 53 | //"read:org", // TODO(d.isaev): add it gracefully: save enabled grants to db and re-authorize only on needed page for needed users 54 | } 55 | } else { 56 | scopes = []string{ 57 | "user:email", 58 | "public_repo", 59 | } 60 | } 61 | 62 | provider := github.New( 63 | key, 64 | secret, 65 | cbHost+cbURL, 66 | scopes..., 67 | ) 68 | return NewAuthorizer(providerName, provider, f.sessFactory, f.log), nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/api/hooks/injector.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import "github.com/golangci/golangci-api/internal/shared/providers/provider" 4 | 5 | type afterProviderCreateFunc func(p provider.Provider) error 6 | 7 | type Injector struct { 8 | afterProviderCreate []afterProviderCreateFunc 9 | } 10 | 11 | func (hi *Injector) AddAfterProviderCreate(hook afterProviderCreateFunc) { 12 | hi.afterProviderCreate = append(hi.afterProviderCreate, hook) 13 | } 14 | 15 | func (hi Injector) RunAfterProviderCreate(p provider.Provider) error { 16 | for _, hook := range hi.afterProviderCreate { 17 | if err := hook(p); err != nil { 18 | return err 19 | } 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /pkg/api/models/auth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | //go:generate goqueryset -in auth.go 10 | 11 | // gen:qs 12 | type Auth struct { 13 | gorm.Model 14 | 15 | AccessToken string 16 | PrivateAccessToken string 17 | 18 | RawData []byte 19 | UserID uint 20 | 21 | Provider string 22 | ProviderUserID uint64 23 | 24 | Login string 25 | } 26 | 27 | func (a Auth) GoString() string { 28 | return fmt.Sprintf("{ID: %d, UserID: %d, Login: %s, Provider: %s}", 29 | a.ID, a.UserID, a.Login, a.Provider) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/api/models/org.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/golangci/golangci-api/internal/api/apierrors" 7 | "github.com/jinzhu/gorm" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | //go:generate goqueryset -in org.go 12 | 13 | // gen:qs 14 | type Org struct { 15 | gorm.Model `json:"-"` 16 | 17 | Name string `json:"-"` 18 | DisplayName string `json:"name"` 19 | 20 | Provider string `json:"provider"` 21 | ProviderID int `json:"-"` 22 | ProviderPersonalUserID int `json:"-"` 23 | 24 | Settings json.RawMessage `json:"settings"` 25 | Version int `json:"version"` 26 | } 27 | 28 | func (o *Org) IsFake() bool { 29 | return o.ProviderPersonalUserID != 0 30 | } 31 | 32 | func (o *Org) UnmarshalSettings() (*OrgSettings, error) { 33 | var s OrgSettings 34 | if err := json.Unmarshal(o.Settings, &s); err != nil { 35 | return nil, errors.Wrapf(err, "failed to unmarshal settings for org(%d)", o.ID) 36 | } 37 | 38 | return &s, nil 39 | } 40 | 41 | func (o *Org) MarshalSettings(v interface{}) error { 42 | data, err := json.Marshal(v) 43 | if err != nil { 44 | return errors.Wrapf(err, "failed to marshal %#v as settings", v) 45 | } 46 | o.Settings = data 47 | return nil 48 | } 49 | 50 | type OrgSeat struct { 51 | Email string `json:"email"` 52 | } 53 | 54 | type OrgSettings struct { 55 | Seats []OrgSeat `json:"seats,omitempty"` 56 | } 57 | 58 | func (u OrgUpdater) UpdateRequired() error { 59 | n, err := u.UpdateNum() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if n == 0 { 65 | return apierrors.NewRaceConditionError("data was changed in parallel request") 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (qs OrgQuerySet) ForProviderRepo(providerName, orgName string, providerOwnerID int) OrgQuerySet { 72 | qs = qs.ProviderEq(providerName) 73 | if orgName == "" { 74 | qs = qs.ProviderPersonalUserIDEq(providerOwnerID) 75 | } else { 76 | qs = qs.ProviderIDEq(providerOwnerID) 77 | } 78 | 79 | return qs 80 | } 81 | -------------------------------------------------------------------------------- /pkg/api/models/payment_gateway_event.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | //go:generate goqueryset -in payment_gateway_event.go 8 | 9 | // gen:qs 10 | type PaymentGatewayEvent struct { 11 | gorm.Model 12 | 13 | Provider string 14 | ProviderID string 15 | 16 | UserID *uint 17 | 18 | Type string 19 | Data []byte 20 | } 21 | -------------------------------------------------------------------------------- /pkg/api/models/pull_request_analysis.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | //go:generate goqueryset -in pull_request_analysis.go 8 | 9 | //gen:qs 10 | type PullRequestAnalysis struct { 11 | gorm.Model 12 | 13 | RepoID uint 14 | PullRequestNumber int 15 | 16 | GithubDeliveryGUID string 17 | 18 | CommitSHA string 19 | 20 | Status string 21 | ReportedIssuesCount int 22 | 23 | ResultJSON []byte 24 | } 25 | 26 | func (PullRequestAnalysis) TableName() string { 27 | return "pull_request_analyzes" 28 | } 29 | -------------------------------------------------------------------------------- /pkg/api/models/repo_analysis.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | //go:generate goqueryset -in repo_analysis.go 10 | 11 | // gen:qs 12 | type RepoAnalysis struct { 13 | gorm.Model 14 | 15 | RepoAnalysisStatusID uint 16 | RepoAnalysisStatus RepoAnalysisStatus 17 | 18 | AnalysisGUID string 19 | Status string 20 | CommitSHA string 21 | ResultJSON json.RawMessage 22 | AttemptNumber int 23 | LintersVersion string 24 | } 25 | 26 | func (RepoAnalysis) TableName() string { 27 | return "repo_analyzes" 28 | } 29 | -------------------------------------------------------------------------------- /pkg/api/models/repo_analysis_status.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | //go:generate goqueryset -in repo_analysis_status.go 10 | 11 | // gen:qs 12 | type RepoAnalysisStatus struct { 13 | gorm.Model 14 | 15 | RepoID uint 16 | 17 | LastAnalyzedAt time.Time 18 | LastAnalyzedLintersVersion string 19 | 20 | HasPendingChanges bool 21 | PendingCommitSHA string 22 | Version int 23 | DefaultBranch string 24 | IsEmpty bool 25 | 26 | Active bool 27 | } 28 | -------------------------------------------------------------------------------- /pkg/api/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | //go:generate goqueryset -in user.go 8 | 9 | // gen:qs 10 | type User struct { 11 | gorm.Model 12 | 13 | Email string 14 | 15 | Name string 16 | AvatarURL string 17 | } 18 | -------------------------------------------------------------------------------- /pkg/api/policy/errors.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import "github.com/golangci/golangci-api/internal/api/apierrors" 4 | 5 | var ErrNotOrgAdmin = apierrors.NewNotAcceptableError("NOT_ORG_ADMIN") 6 | var ErrNotOrgMember = apierrors.NewNotAcceptableError("NOT_ORG_MEMBER") 7 | var ErrNoActiveSubscription = apierrors.NewNotAcceptableError("NOT_ACTIVE_SUBSCRIPTION") 8 | var ErrNoSeatInSubscription = apierrors.NewNotAcceptableError("NOT_SEAT_IN_SUBSCRIPTION") 9 | -------------------------------------------------------------------------------- /pkg/api/request/body.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/golangci/golangci-api/internal/shared/logutil" 4 | 5 | type Body []byte 6 | 7 | func (b Body) FillLogContext(lctx logutil.Context) { 8 | } 9 | -------------------------------------------------------------------------------- /pkg/api/request/context.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/golangci/golangci-api/pkg/api/auth" 8 | 9 | "github.com/golangci/golangci-api/internal/api/session" 10 | "github.com/golangci/golangci-api/internal/shared/logutil" 11 | "github.com/jinzhu/gorm" 12 | ) 13 | 14 | type Context interface { 15 | RequestStartedAt() time.Time 16 | Logger() logutil.Log 17 | SessContext() *session.RequestContext 18 | } 19 | 20 | type BaseContext struct { 21 | Ctx context.Context 22 | Log logutil.Log 23 | Lctx logutil.Context 24 | DB *gorm.DB 25 | 26 | StartedAt time.Time 27 | 28 | SessCtx *session.RequestContext 29 | } 30 | 31 | func (ctx BaseContext) RequestStartedAt() time.Time { 32 | return ctx.StartedAt 33 | } 34 | 35 | func (ctx BaseContext) Logger() logutil.Log { 36 | return ctx.Log 37 | } 38 | 39 | func (ctx BaseContext) SessContext() *session.RequestContext { 40 | return ctx.SessCtx 41 | } 42 | 43 | type AnonymousContext struct { 44 | BaseContext 45 | } 46 | 47 | // InternalContext used for internal requests and provides internal authorization 48 | type InternalContext struct { 49 | BaseContext 50 | } 51 | 52 | type AuthorizedContext struct { 53 | BaseContext 54 | 55 | auth.AuthenticatedUser 56 | } 57 | 58 | func (ac AuthorizedContext) ToAnonumousContext() *AnonymousContext { 59 | return &AnonymousContext{ 60 | BaseContext: ac.BaseContext, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/api/request/org.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/golangci/golangci-api/internal/shared/logutil" 4 | 5 | type OrgID struct { 6 | OrgID uint `request:"org_id,urlPart,"` 7 | } 8 | 9 | func (o OrgID) FillLogContext(lctx logutil.Context) { 10 | lctx["org_id"] = o.OrgID 11 | } 12 | 13 | type Org struct { 14 | Provider string `request:",urlPart,"` 15 | Name string `request:",urlPart,"` 16 | } 17 | 18 | func (o Org) FillLogContext(lctx logutil.Context) { 19 | lctx["org_provider"] = o.Provider 20 | lctx["org_name"] = o.Name 21 | } 22 | 23 | type SubID struct { 24 | SubID uint `request:"sub_id,urlPart,"` 25 | } 26 | 27 | func (s SubID) FillLogContext(lctx logutil.Context) { 28 | lctx["sub_id"] = s.SubID 29 | } 30 | 31 | type OrgSubID struct { 32 | OrgID 33 | SubID 34 | } 35 | 36 | func (os OrgSubID) FillLogContext(lctx logutil.Context) { 37 | os.OrgID.FillLogContext(lctx) 38 | os.SubID.FillLogContext(lctx) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/api/request/repo.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/golangci/golangci-api/internal/shared/logutil" 9 | ) 10 | 11 | type Repo struct { 12 | Provider string `request:",urlPart,"` 13 | Owner string `request:",urlPart,"` 14 | Name string `request:",urlPart,"` 15 | } 16 | 17 | func (r Repo) FullName() string { 18 | return strings.ToLower(fmt.Sprintf("%s/%s", r.Owner, r.Name)) 19 | } 20 | 21 | func (r Repo) FullNameWithProvider() string { 22 | return strings.ToLower(fmt.Sprintf("%s/%s/%s", r.Provider, r.Owner, r.Name)) 23 | } 24 | 25 | func (r Repo) String() string { 26 | return fmt.Sprintf("%s/%s", r.Provider, r.FullName()) 27 | } 28 | 29 | func (r Repo) FillLogContext(lctx logutil.Context) { 30 | lctx["repo"] = r.String() 31 | } 32 | 33 | type ShortRepo struct { 34 | Owner string `request:",urlPart,"` 35 | Name string `request:",urlPart,"` 36 | } 37 | 38 | func (r ShortRepo) FullName() string { 39 | return strings.ToLower(fmt.Sprintf("%s/%s", r.Owner, r.Name)) 40 | } 41 | 42 | func (r ShortRepo) String() string { 43 | return r.FullName() 44 | } 45 | 46 | func (r ShortRepo) FillLogContext(lctx logutil.Context) { 47 | lctx["repo"] = r.String() 48 | } 49 | 50 | type BodyRepo struct { 51 | Provider string 52 | Owner string 53 | Name string 54 | } 55 | 56 | func (r BodyRepo) FullName() string { 57 | return strings.ToLower(fmt.Sprintf("%s/%s", r.Owner, r.Name)) 58 | } 59 | 60 | func (r BodyRepo) String() string { 61 | return fmt.Sprintf("%s/%s", r.Provider, r.FullName()) 62 | } 63 | 64 | func (r BodyRepo) FillLogContext(lctx logutil.Context) { 65 | lctx["repo"] = r.String() 66 | } 67 | 68 | type RepoID struct { 69 | ID uint `request:"repoID,urlPart,"` 70 | } 71 | 72 | func (r RepoID) FillLogContext(lctx logutil.Context) { 73 | lctx["repoID"] = strconv.Itoa(int(r.ID)) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/api/returntypes/returntypes.go: -------------------------------------------------------------------------------- 1 | package returntypes 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Error struct { 8 | Error string `json:"error,omitempty"` 9 | } 10 | 11 | type RepoInfo struct { 12 | ID uint `json:"id"` 13 | HookID string `json:"hookId"` // needed only for tests 14 | Name string `json:"name"` 15 | Organization string `json:"organization,omitempty"` 16 | IsAdmin bool `json:"isAdmin"` 17 | IsActivated bool `json:"isActivated,omitempty"` 18 | IsPrivate bool `json:"isPrivate,omitempty"` 19 | IsCreating bool `json:"isCreating,omitempty"` 20 | IsDeleting bool `json:"isDeleting,omitempty"` 21 | Language string `json:"language,omitempty"` 22 | CreateFailReason string `json:"createFailReason,omitempty"` 23 | } 24 | 25 | type WrappedRepoInfo struct { 26 | Repo RepoInfo `json:"repo"` 27 | } 28 | 29 | type OrgInfo struct { 30 | Provider string `json:"provider"` 31 | Name string `json:"name"` 32 | HasActiveSubscription bool `json:"hasActiveSubscription"` 33 | CanModify bool `json:"canModify"` 34 | CantModifyReason string `json:"cantModifyReason"` 35 | } 36 | 37 | type RepoListResponse struct { 38 | Repos []RepoInfo `json:"repos"` 39 | PrivateRepos []RepoInfo `json:"privateRepos"` 40 | PrivateReposWereFetched bool `json:"privateReposWereFetched"` 41 | Organizations map[string]OrgInfo `json:"organizations"` 42 | } 43 | 44 | type AuthorizedUser struct { 45 | ID uint `json:"id"` 46 | Email string `json:"email"` 47 | Name string `json:"name"` 48 | AvatarURL string `json:"avatarUrl"` 49 | GithubLogin string `json:"githubLogin"` 50 | CreatedAt time.Time `json:"createdAt"` 51 | } 52 | 53 | type CheckAuthResponse struct { 54 | User AuthorizedUser `json:"user"` 55 | } 56 | 57 | type SubInfo struct { 58 | SeatsCount int `json:"seatsCount"` 59 | Status string `json:"status"` 60 | Version int `json:"version"` 61 | PricePerSeat string `json:"pricePerSeat"` 62 | CancelURL string `json:"cancelUrl"` 63 | 64 | TrialAllowanceInDays int `json:"trialAllowanceInDays"` 65 | PaddleTrialDaysAuth string `json:"paddleTrialDaysAuth"` 66 | } 67 | 68 | type IDResponse struct { 69 | ID int `json:"id"` 70 | } 71 | -------------------------------------------------------------------------------- /pkg/api/services/events/endpoint.go: -------------------------------------------------------------------------------- 1 | // Code generated by genservices. DO NOT EDIT. 2 | package events 3 | 4 | import ( 5 | "context" 6 | "runtime/debug" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | "github.com/golangci/golangci-api/internal/api/apierrors" 10 | "github.com/golangci/golangci-api/internal/api/endpointutil" 11 | "github.com/golangci/golangci-api/internal/shared/logutil" 12 | "github.com/golangci/golangci-api/pkg/api/request" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type TrackEventRequest struct { 17 | Req *Request 18 | } 19 | 20 | type TrackEventResponse struct { 21 | err error 22 | } 23 | 24 | func makeTrackEventEndpoint(svc Service, log logutil.Log) endpoint.Endpoint { 25 | return func(ctx context.Context, reqObj interface{}) (resp interface{}, err error) { 26 | 27 | req := reqObj.(TrackEventRequest) 28 | 29 | reqLogger := log 30 | defer func() { 31 | if rerr := recover(); rerr != nil { 32 | reqLogger.Errorf("Panic occurred") 33 | reqLogger.Infof("%s", debug.Stack()) 34 | resp = TrackEventResponse{ 35 | err: errors.New("panic occurred"), 36 | } 37 | err = nil 38 | } 39 | }() 40 | 41 | if err := endpointutil.Error(ctx); err != nil { 42 | log.Warnf("Error occurred during request context creation: %s", err) 43 | resp = TrackEventResponse{ 44 | err: err, 45 | } 46 | return resp, nil 47 | } 48 | 49 | rc := endpointutil.RequestContext(ctx).(*request.AuthorizedContext) 50 | reqLogger = rc.Log 51 | 52 | req.Req.FillLogContext(rc.Lctx) 53 | 54 | err = svc.TrackEvent(rc, req.Req) 55 | if err != nil { 56 | if !apierrors.IsErrorLikeResult(err) { 57 | rc.Log.Errorf("events.Service.TrackEvent failed: %s", err) 58 | } 59 | return TrackEventResponse{err}, nil 60 | } 61 | 62 | return TrackEventResponse{nil}, nil 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/api/services/events/service.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | sharedevents "github.com/golangci/golangci-api/internal/api/events" 5 | "github.com/golangci/golangci-api/internal/shared/logutil" 6 | "github.com/golangci/golangci-api/pkg/api/request" 7 | ) 8 | 9 | type Request struct { 10 | Name string 11 | Payload map[string]interface{} 12 | } 13 | 14 | func (r Request) FillLogContext(lctx logutil.Context) { 15 | lctx["event_name"] = r.Name 16 | for k, v := range r.Payload { 17 | lctx[k] = v 18 | } 19 | } 20 | 21 | type Service interface { 22 | //url:/v1/events/analytics method:POST 23 | TrackEvent(rc *request.AuthorizedContext, req *Request) error 24 | } 25 | 26 | type BasicService struct{} 27 | 28 | func (s BasicService) TrackEvent(rc *request.AuthorizedContext, req *Request) error { 29 | sharedevents.NewAuthenticatedTracker(int(rc.Auth.UserID)).Track(rc.Ctx, req.Name, req.Payload) 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/api/services/repohook/endpoint.go: -------------------------------------------------------------------------------- 1 | // Code generated by genservices. DO NOT EDIT. 2 | package repohook 3 | 4 | import ( 5 | "context" 6 | "runtime/debug" 7 | 8 | "github.com/go-kit/kit/endpoint" 9 | "github.com/golangci/golangci-api/internal/api/apierrors" 10 | "github.com/golangci/golangci-api/internal/api/endpointutil" 11 | "github.com/golangci/golangci-api/internal/shared/logutil" 12 | "github.com/golangci/golangci-api/pkg/api/request" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type HandleGithubWebhookRequest struct { 17 | ReqRepo *GithubWebhook 18 | Body request.Body 19 | } 20 | 21 | type HandleGithubWebhookResponse struct { 22 | err error 23 | } 24 | 25 | func makeHandleGithubWebhookEndpoint(svc Service, log logutil.Log) endpoint.Endpoint { 26 | return func(ctx context.Context, reqObj interface{}) (resp interface{}, err error) { 27 | 28 | req := reqObj.(HandleGithubWebhookRequest) 29 | 30 | reqLogger := log 31 | defer func() { 32 | if rerr := recover(); rerr != nil { 33 | reqLogger.Errorf("Panic occurred") 34 | reqLogger.Infof("%s", debug.Stack()) 35 | resp = HandleGithubWebhookResponse{ 36 | err: errors.New("panic occurred"), 37 | } 38 | err = nil 39 | } 40 | }() 41 | 42 | if err := endpointutil.Error(ctx); err != nil { 43 | log.Warnf("Error occurred during request context creation: %s", err) 44 | resp = HandleGithubWebhookResponse{ 45 | err: err, 46 | } 47 | return resp, nil 48 | } 49 | 50 | rc := endpointutil.RequestContext(ctx).(*request.AnonymousContext) 51 | reqLogger = rc.Log 52 | 53 | req.ReqRepo.FillLogContext(rc.Lctx) 54 | req.Body.FillLogContext(rc.Lctx) 55 | 56 | err = svc.HandleGithubWebhook(rc, req.ReqRepo, req.Body) 57 | if err != nil { 58 | if !apierrors.IsErrorLikeResult(err) { 59 | rc.Log.Errorf("repohook.Service.HandleGithubWebhook failed: %s", err) 60 | } 61 | return HandleGithubWebhookResponse{err}, nil 62 | } 63 | 64 | return HandleGithubWebhookResponse{nil}, nil 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/api/workers/primaryqueue/config.go: -------------------------------------------------------------------------------- 1 | package primaryqueue 2 | 3 | import "time" 4 | 5 | const VisibilityTimeoutSec = 60 // must be in sync with cloudformation.yml 6 | const ConsumerTimeout = 45 * time.Second // reserve 15 sec 7 | -------------------------------------------------------------------------------- /pkg/api/workers/primaryqueue/helpers.go: -------------------------------------------------------------------------------- 1 | package primaryqueue 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/queue/consumers" 5 | "github.com/pkg/errors" 6 | redsync "gopkg.in/redsync.v1" 7 | ) 8 | 9 | func RegisterConsumer(consumeFunc interface{}, queueID string, m *consumers.Multiplexer, df *redsync.Redsync) error { 10 | consumer, err := consumers.NewReflectConsumer(consumeFunc, ConsumerTimeout, df) 11 | if err != nil { 12 | return errors.Wrap(err, "can't make reflect consumer") 13 | } 14 | 15 | return m.RegisterConsumer(queueID, consumer) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/api/workers/primaryqueue/repos/collaborator_util.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/golangci/golangci-api/internal/shared/config" 8 | "github.com/golangci/golangci-api/internal/shared/providers/implementations" 9 | ) 10 | 11 | func getReviewerLogin(providerName string, cfg config.Config) string { 12 | if providerName == implementations.GithubProviderName { 13 | providerName = "github" // TODO: rename config var 14 | } 15 | key := fmt.Sprintf("%s_REVIEWER_LOGIN", strings.ToUpper(providerName)) 16 | return cfg.GetString(key) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/goenvbuild/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type Service struct { 11 | ProjectPath string `mapstructure:"project-path"` 12 | AnalyzedPaths []string `mapstructure:"analyzed-paths"` 13 | 14 | GolangciLintVersion string `mapstructure:"golangci-lint-version"` 15 | Prepare []string 16 | SuggestedChanges SuggestedChangesConfig `mapstructure:"suggested-changes"` 17 | } 18 | 19 | type SuggestedChangesConfig struct { 20 | Disabled bool 21 | } 22 | 23 | type FullConfig struct { 24 | Service Service 25 | } 26 | 27 | func (cfg *Service) validateAnalyzedPaths() error { 28 | for _, path := range cfg.AnalyzedPaths { 29 | if strings.HasPrefix(path, "/") { 30 | return fmt.Errorf("path %q is invalid: only relative paths are allowed", path) 31 | } 32 | 33 | path = strings.TrimSuffix(path, "/...") 34 | if strings.Contains(path, "..") { 35 | return fmt.Errorf("path %q is invalid: analyzing of parent dirs (..) isn't allowed", path) 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func (cfg *Service) GetValidatedAnalyzedPaths() ([]string, error) { 43 | defaultPaths := []string{"./..."} 44 | if cfg == nil { 45 | return defaultPaths, nil 46 | } 47 | 48 | if err := cfg.validateAnalyzedPaths(); err != nil { 49 | return nil, errors.Wrap(err, "failed to validate service config") 50 | } 51 | 52 | if len(cfg.AnalyzedPaths) != 0 { 53 | return cfg.AnalyzedPaths, nil 54 | } 55 | 56 | return defaultPaths, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/goenvbuild/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 8 | ) 9 | 10 | type StepGroupLogger struct { 11 | sg *result.StepGroup 12 | } 13 | 14 | func NewStepGroupLogger(sg *result.StepGroup) *StepGroupLogger { 15 | return &StepGroupLogger{ 16 | sg: sg, 17 | } 18 | } 19 | 20 | func (sgl StepGroupLogger) lastStep() *result.Step { 21 | return sgl.sg.LastStep() 22 | } 23 | 24 | func (sgl StepGroupLogger) Fatalf(format string, args ...interface{}) { 25 | log.Fatalf(format, args...) 26 | } 27 | func (sgl StepGroupLogger) Errorf(format string, args ...interface{}) { 28 | s := sgl.lastStep() 29 | s.AddOutputLine("[ERROR] "+format, args...) 30 | } 31 | 32 | func (sgl StepGroupLogger) Warnf(format string, args ...interface{}) { 33 | s := sgl.lastStep() 34 | s.AddOutputLine("[WARN] "+format, args...) 35 | } 36 | func (sgl StepGroupLogger) Infof(format string, args ...interface{}) { 37 | s := sgl.lastStep() 38 | s.AddOutputLine(format, args...) 39 | } 40 | 41 | func (sgl StepGroupLogger) Debugf(key string, format string, args ...interface{}) { 42 | } 43 | 44 | func (sgl StepGroupLogger) Child(name string) logutil.Log { 45 | panic("child isn't supported") 46 | } 47 | 48 | func (sgl StepGroupLogger) SetLevel(level logutil.LogLevel) { 49 | panic("setlevel isn't supported") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/goenvbuild/packages/exclude.go: -------------------------------------------------------------------------------- 1 | package packages 2 | 3 | var StdExcludeDirRegexps = []string{ 4 | "vendor$", "third_party$", 5 | "testdata$", "examples$", 6 | "Godeps$", 7 | "builtin$", 8 | } 9 | -------------------------------------------------------------------------------- /pkg/goenvbuild/packages/package.go: -------------------------------------------------------------------------------- 1 | package packages 2 | 3 | import ( 4 | "go/build" 5 | "path/filepath" 6 | ) 7 | 8 | type Package struct { 9 | bp *build.Package 10 | 11 | isFake bool 12 | dir string // dir != bp.dir only if isFake == true 13 | } 14 | 15 | func isCgoFilename(f string) bool { 16 | return filepath.Base(f) == "C" 17 | } 18 | 19 | func (pkg *Package) Files(includeTest bool) []string { 20 | var pkgFiles []string 21 | for _, f := range pkg.bp.GoFiles { 22 | if !isCgoFilename(f) { 23 | // skip cgo at all levels to prevent failures on file reading 24 | pkgFiles = append(pkgFiles, f) 25 | } 26 | } 27 | 28 | // TODO: add cgo files 29 | if includeTest { 30 | pkgFiles = append(pkgFiles, pkg.TestFiles()...) 31 | } 32 | 33 | for i, f := range pkgFiles { 34 | pkgFiles[i] = filepath.Join(pkg.bp.Dir, f) 35 | } 36 | 37 | return pkgFiles 38 | } 39 | 40 | func (pkg *Package) Dir() string { 41 | if pkg.dir != "" { // for fake packages 42 | return pkg.dir 43 | } 44 | 45 | return pkg.bp.Dir 46 | } 47 | 48 | func (pkg *Package) IsTestOnly() bool { 49 | return len(pkg.bp.GoFiles) == 0 50 | } 51 | 52 | func (pkg *Package) TestFiles() []string { 53 | var pkgFiles []string 54 | pkgFiles = append(pkgFiles, pkg.bp.TestGoFiles...) 55 | pkgFiles = append(pkgFiles, pkg.bp.XTestGoFiles...) 56 | return pkgFiles 57 | } 58 | 59 | func (pkg *Package) BuildPackage() *build.Package { 60 | return pkg.bp 61 | } 62 | -------------------------------------------------------------------------------- /pkg/goenvbuild/packages/program.go: -------------------------------------------------------------------------------- 1 | package packages 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | ) 7 | 8 | type Program struct { 9 | packages []Package 10 | 11 | bctx build.Context 12 | } 13 | 14 | func (p *Program) String() string { 15 | files := p.Files(true) 16 | if len(files) == 1 { 17 | return files[0] 18 | } 19 | 20 | return fmt.Sprintf("%s", p.Dirs()) 21 | } 22 | 23 | func (p *Program) BuildContext() *build.Context { 24 | return &p.bctx 25 | } 26 | 27 | func (p Program) Packages() []Package { 28 | return p.packages 29 | } 30 | 31 | func (p *Program) addPackage(pkg *Package) { 32 | packagesToAdd := []Package{*pkg} 33 | if len(pkg.bp.XTestGoFiles) != 0 { 34 | // create separate package because xtest files have different package name 35 | xbp := build.Package{ 36 | Dir: pkg.bp.Dir, 37 | ImportPath: pkg.bp.ImportPath + "_test", 38 | XTestGoFiles: pkg.bp.XTestGoFiles, 39 | XTestImportPos: pkg.bp.XTestImportPos, 40 | XTestImports: pkg.bp.XTestImports, 41 | } 42 | packagesToAdd = append(packagesToAdd, Package{ 43 | bp: &xbp, 44 | }) 45 | pkg.bp.XTestGoFiles = nil 46 | pkg.bp.XTestImportPos = nil 47 | pkg.bp.XTestImports = nil 48 | } 49 | 50 | p.packages = append(p.packages, packagesToAdd...) 51 | } 52 | 53 | func (p *Program) Files(includeTest bool) []string { 54 | var ret []string 55 | for _, pkg := range p.packages { 56 | ret = append(ret, pkg.Files(includeTest)...) 57 | } 58 | 59 | return ret 60 | } 61 | 62 | func (p *Program) Dirs() []string { 63 | var ret []string 64 | for _, pkg := range p.packages { 65 | if !pkg.isFake { 66 | ret = append(ret, pkg.Dir()) 67 | } 68 | } 69 | 70 | return ret 71 | } 72 | -------------------------------------------------------------------------------- /pkg/goenvbuild/repoinfo/info.go: -------------------------------------------------------------------------------- 1 | package repoinfo 2 | 3 | type Info struct { 4 | CanonicalImportPath string 5 | CanonicalImportPathReason string 6 | } 7 | -------------------------------------------------------------------------------- /pkg/goenvbuild/result/run.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func (lg *Log) RunNewGroup(name string, f func(sg *StepGroup) error) error { 10 | sg := lg.AddStepGroup(name) 11 | 12 | startedAt := time.Now() 13 | err := f(sg) 14 | sg.Duration = time.Since(startedAt) 15 | 16 | if err != nil { 17 | sg.LastStep().AddError(err.Error()) 18 | return errors.Wrapf(err, "%s failed", name) 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func (lg *Log) RunNewGroupVoid(name string, f func(sg *StepGroup)) { 25 | _ = lg.RunNewGroup(name, func(sg *StepGroup) error { 26 | f(sg) 27 | return nil 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/worker/analytics/amplitude.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/golangci/golangci-api/pkg/worker/lib/runmode" 8 | amplitude "github.com/savaki/amplitude-go" 9 | ) 10 | 11 | var amplitudeClient *amplitude.Client 12 | var amplitudeClientOnce sync.Once 13 | 14 | func getAmplitudeClient() *amplitude.Client { 15 | amplitudeClientOnce.Do(func() { 16 | if runmode.IsProduction() { 17 | apiKey := os.Getenv("AMPLITUDE_API_KEY") 18 | amplitudeClient = amplitude.New(apiKey) 19 | } 20 | }) 21 | 22 | return amplitudeClient 23 | } 24 | -------------------------------------------------------------------------------- /pkg/worker/analytics/context.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import "context" 4 | 5 | func GetTracker(_ context.Context) Tracker { 6 | return amplitudeMixpanelTracker{} 7 | } 8 | 9 | type trackingContextKeyType string 10 | 11 | const trackingContextKey trackingContextKeyType = "tracking context" 12 | 13 | func ContextWithTrackingProps(ctx context.Context, props map[string]interface{}) context.Context { 14 | return context.WithValue(ctx, trackingContextKey, props) 15 | } 16 | 17 | func getTrackingProps(ctx context.Context) map[string]interface{} { 18 | tp := ctx.Value(trackingContextKey) 19 | if tp == nil { 20 | return map[string]interface{}{} 21 | } 22 | 23 | return tp.(map[string]interface{}) 24 | } 25 | 26 | func ContextWithEventPropsCollector(ctx context.Context, name EventName) context.Context { 27 | return context.WithValue(ctx, name, map[string]interface{}{}) 28 | } 29 | 30 | func SaveEventProp(ctx context.Context, name EventName, key string, value interface{}) { 31 | ec := ctx.Value(name).(map[string]interface{}) 32 | ec[key] = value 33 | } 34 | 35 | func SaveEventProps(ctx context.Context, name EventName, props map[string]interface{}) { 36 | ec := ctx.Value(name).(map[string]interface{}) 37 | 38 | for k, v := range props { 39 | ec[k] = v 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/worker/analytics/errors.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/config" 7 | "github.com/golangci/golangci-api/internal/shared/logutil" 8 | 9 | "github.com/golangci/golangci-api/internal/shared/apperrors" 10 | "github.com/golangci/golangci-api/pkg/worker/lib/runmode" 11 | ) 12 | 13 | func trackError(ctx context.Context, err error, level apperrors.Level) { 14 | if !runmode.IsProduction() { 15 | return 16 | } 17 | 18 | log := logutil.NewStderrLog("trackError") 19 | cfg := config.NewEnvConfig(log) 20 | et := apperrors.GetTracker(cfg, log, "worker") 21 | et.Track(level, err.Error(), getTrackingProps(ctx)) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/worker/analytics/logger.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/golangci/golangci-api/internal/shared/apperrors" 9 | "github.com/golangci/golangci-api/pkg/worker/lib/runmode" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var initLogrusOnce sync.Once 14 | 15 | func initLogrus() { 16 | level := logrus.InfoLevel 17 | if runmode.IsDebug() { 18 | level = logrus.DebugLevel 19 | } 20 | logrus.SetLevel(level) 21 | } 22 | 23 | type Logger interface { 24 | Warnf(format string, args ...interface{}) 25 | Errorf(format string, args ...interface{}) 26 | Infof(format string, args ...interface{}) 27 | Debugf(format string, args ...interface{}) 28 | } 29 | 30 | type logger struct { 31 | ctx context.Context 32 | } 33 | 34 | func (log logger) le() *logrus.Entry { 35 | return logrus.WithFields(getTrackingProps(log.ctx)) 36 | } 37 | 38 | func (log logger) Warnf(format string, args ...interface{}) { 39 | err := fmt.Errorf(format, args...) 40 | log.le().Warn(err.Error()) 41 | trackError(log.ctx, err, apperrors.LevelWarn) 42 | } 43 | 44 | func (log logger) Errorf(format string, args ...interface{}) { 45 | err := fmt.Errorf(format, args...) 46 | log.le().Error(err.Error()) 47 | trackError(log.ctx, err, apperrors.LevelError) 48 | } 49 | 50 | func (log logger) Infof(format string, args ...interface{}) { 51 | log.le().Infof(format, args...) 52 | } 53 | 54 | func (log logger) Debugf(format string, args ...interface{}) { 55 | log.le().Debugf(format, args...) 56 | } 57 | 58 | func Log(ctx context.Context) Logger { 59 | initLogrusOnce.Do(initLogrus) 60 | 61 | return logger{ 62 | ctx: ctx, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/worker/analytics/mixpanel.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/dukex/mixpanel" 8 | "github.com/golangci/golangci-api/pkg/worker/lib/runmode" 9 | ) 10 | 11 | var mixpanelClient mixpanel.Mixpanel 12 | var mixpanelClientOnce sync.Once 13 | 14 | func getMixpanelClient() mixpanel.Mixpanel { 15 | mixpanelClientOnce.Do(func() { 16 | if runmode.IsProduction() { 17 | apiKey := os.Getenv("MIXPANEL_API_KEY") 18 | mixpanelClient = mixpanel.New(apiKey, "") 19 | } 20 | }) 21 | 22 | return mixpanelClient 23 | } 24 | -------------------------------------------------------------------------------- /pkg/worker/analytics/track.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dukex/mixpanel" 7 | amplitude "github.com/savaki/amplitude-go" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type EventName string 12 | 13 | const EventPRChecked EventName = "PR checked" 14 | const EventRepoAnalyzed EventName = "Repo analyzed" 15 | 16 | type Tracker interface { 17 | Track(ctx context.Context, event EventName) 18 | } 19 | 20 | type amplitudeMixpanelTracker struct{} 21 | 22 | func (t amplitudeMixpanelTracker) Track(ctx context.Context, eventName EventName) { 23 | trackingProps := getTrackingProps(ctx) 24 | userID := trackingProps["userIDString"].(string) 25 | 26 | eventProps := map[string]interface{}{} 27 | for k, v := range trackingProps { 28 | if k != "userIDString" { 29 | eventProps[k] = v 30 | } 31 | } 32 | 33 | addedEventProps := ctx.Value(eventName).(map[string]interface{}) 34 | for k, v := range addedEventProps { 35 | eventProps[k] = v 36 | } 37 | log.Infof("track event %s with props %+v", eventName, eventProps) 38 | 39 | ac := getAmplitudeClient() 40 | if ac != nil { 41 | ev := amplitude.Event{ 42 | UserId: userID, 43 | EventType: string(eventName), 44 | EventProperties: eventProps, 45 | } 46 | if err := ac.Publish(ev); err != nil { 47 | Log(ctx).Warnf("Can't publish %+v to amplitude: %s", ev, err) 48 | } 49 | } 50 | 51 | mp := getMixpanelClient() 52 | if mp != nil { 53 | const ip = "0" // don't auto-detect 54 | ev := &mixpanel.Event{ 55 | IP: ip, 56 | Properties: eventProps, 57 | } 58 | if err := mp.Track(userID, string(eventName), ev); err != nil { 59 | Log(ctx).Warnf("Can't publish event %s (%+v) to mixpanel: %s", string(eventName), ev, err) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzequeue/consumers/analyze_pr_test.go: -------------------------------------------------------------------------------- 1 | package consumers 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/golangci/golangci-api/pkg/worker/lib/experiments" 11 | 12 | "github.com/golangci/golangci-api/internal/shared/apperrors" 13 | 14 | "github.com/golangci/golangci-api/internal/shared/config" 15 | 16 | "github.com/golangci/golangci-api/internal/shared/logutil" 17 | "github.com/golangci/golangci-api/pkg/worker/analyze/processors" 18 | 19 | "github.com/golangci/golangci-api/pkg/worker/test" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestAnalyzeRepo(t *testing.T) { 24 | test.MarkAsSlow(t) 25 | test.Init() 26 | 27 | prNumber := 1 28 | if pr := os.Getenv("PR"); pr != "" { 29 | var err error 30 | prNumber, err = strconv.Atoi(pr) 31 | assert.NoError(t, err) 32 | } 33 | const userID = 1 34 | 35 | repoOwner := "golangci" 36 | repoName := "golangci-worker" 37 | if r := os.Getenv("REPO"); r != "" { 38 | parts := strings.SplitN(r, "/", 2) 39 | repoOwner, repoName = parts[0], parts[1] 40 | } 41 | 42 | pf := processors.NewBasicPullProcessorFactory(&processors.BasicPullConfig{}) 43 | log := logutil.NewStderrLog("") 44 | cfg := config.NewEnvConfig(log) 45 | errTracker := apperrors.NewNopTracker() 46 | ec := experiments.NewChecker(cfg, log) 47 | 48 | err := NewAnalyzePR(pf, log, errTracker, cfg, ec).Consume(context.Background(), repoOwner, repoName, 49 | false, cfg.GetString("TEST_GITHUB_TOKEN"), prNumber, "", userID, "test-guid", "commit-sha") 50 | assert.NoError(t, err) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzequeue/consumers/errors.go: -------------------------------------------------------------------------------- 1 | package consumers 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/pkg/worker/analyze/processors" 5 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 6 | "github.com/golangci/golangci-api/pkg/worker/lib/fetchers" 7 | "github.com/golangci/golangci-api/pkg/worker/lib/github" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func isRecoverableError(err error) bool { 12 | err = errors.Cause(err) 13 | if err == executors.ErrExecutorFail { 14 | return true 15 | } 16 | 17 | if err == processors.ErrUnrecoverable { 18 | return false 19 | } 20 | 21 | if err == fetchers.ErrNoBranchOrRepo || err == fetchers.ErrNoCommit { 22 | return false 23 | } 24 | 25 | if err == processors.ErrNothingToAnalyze { 26 | return false 27 | } 28 | 29 | return github.IsRecoverableError(err) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzequeue/task/task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "github.com/golangci/golangci-api/pkg/worker/lib/github" 4 | 5 | type PRAnalysis struct { 6 | github.Context 7 | APIRequestID string 8 | UserID uint 9 | AnalysisGUID string 10 | } 11 | 12 | type RepoAnalysis struct { 13 | Name string 14 | AnalysisGUID string 15 | Branch string 16 | } 17 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/config.go: -------------------------------------------------------------------------------- 1 | package analyzesqueue 2 | 3 | import "time" 4 | 5 | const VisibilityTimeoutSec = 600 // must be in sync with cloudformation.yml 6 | const ConsumerTimeout = 530 * time.Second // reserve 30 sec 7 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/helpers.go: -------------------------------------------------------------------------------- 1 | package analyzesqueue 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/queue/consumers" 5 | "github.com/pkg/errors" 6 | redsync "gopkg.in/redsync.v1" 7 | ) 8 | 9 | func RegisterConsumer(consumeFunc interface{}, queueID string, m *consumers.Multiplexer, df *redsync.Redsync) error { 10 | consumer, err := consumers.NewReflectConsumer(consumeFunc, ConsumerTimeout, df) 11 | if err != nil { 12 | return errors.Wrap(err, "can't make reflect consumer") 13 | } 14 | 15 | return m.RegisterConsumer(queueID, consumer) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/pullanalyzesqueue/config.go: -------------------------------------------------------------------------------- 1 | package pullanalyzesqueue 2 | 3 | import "github.com/golangci/golangci-api/pkg/worker/lib/github" 4 | 5 | const runQueueID = "analyzes/pull/run" 6 | 7 | type RunMessage struct { 8 | github.Context 9 | APIRequestID string 10 | UserID uint 11 | AnalysisGUID string 12 | CommitSHA string 13 | } 14 | 15 | func (m RunMessage) LockID() string { 16 | return m.AnalysisGUID 17 | } 18 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/pullanalyzesqueue/consumer.go: -------------------------------------------------------------------------------- 1 | package pullanalyzesqueue 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/queue/consumers" 7 | analyzesConsumers "github.com/golangci/golangci-api/pkg/worker/analyze/analyzequeue/consumers" 8 | "github.com/golangci/golangci-api/pkg/worker/analyze/analyzesqueue" 9 | redsync "gopkg.in/redsync.v1" 10 | ) 11 | 12 | type Consumer struct { 13 | subConsumer *analyzesConsumers.AnalyzePR 14 | } 15 | 16 | func NewConsumer(subConsumer *analyzesConsumers.AnalyzePR) *Consumer { 17 | return &Consumer{ 18 | subConsumer: subConsumer, 19 | } 20 | } 21 | 22 | func (c Consumer) Register(m *consumers.Multiplexer, df *redsync.Redsync) error { 23 | return analyzesqueue.RegisterConsumer(c.consumeMessage, runQueueID, m, df) 24 | } 25 | 26 | func (c Consumer) consumeMessage(ctx context.Context, m *RunMessage) error { 27 | return c.subConsumer.Consume(ctx, m.Repo.Owner, m.Repo.Name, 28 | m.Repo.IsPrivate, m.GithubAccessToken, 29 | m.PullRequestNumber, m.APIRequestID, m.UserID, m.AnalysisGUID, m.CommitSHA) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/pullanalyzesqueue/producer.go: -------------------------------------------------------------------------------- 1 | package pullanalyzesqueue 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/queue" 5 | "github.com/golangci/golangci-api/internal/shared/queue/producers" 6 | ) 7 | 8 | type Producer struct { 9 | producers.Base 10 | } 11 | 12 | func (p *Producer) Register(m *producers.Multiplexer) error { 13 | return p.Base.Register(m, runQueueID) 14 | } 15 | 16 | func (p Producer) Put(m queue.Message) error { 17 | return p.Base.Put(m) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/repoanalyzesqueue/config.go: -------------------------------------------------------------------------------- 1 | package repoanalyzesqueue 2 | 3 | const runQueueID = "analyzes/repo/run" 4 | 5 | type runMessage struct { 6 | RepoName string 7 | AnalysisGUID string 8 | Branch string 9 | PrivateAccessToken string 10 | CommitSHA string 11 | } 12 | 13 | func (m runMessage) LockID() string { 14 | return m.AnalysisGUID 15 | } 16 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/repoanalyzesqueue/consumer.go: -------------------------------------------------------------------------------- 1 | package repoanalyzesqueue 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/queue/consumers" 7 | analyzesConsumers "github.com/golangci/golangci-api/pkg/worker/analyze/analyzequeue/consumers" 8 | "github.com/golangci/golangci-api/pkg/worker/analyze/analyzesqueue" 9 | redsync "gopkg.in/redsync.v1" 10 | ) 11 | 12 | type Consumer struct { 13 | subConsumer *analyzesConsumers.AnalyzeRepo 14 | } 15 | 16 | func NewConsumer(subConsumer *analyzesConsumers.AnalyzeRepo) *Consumer { 17 | return &Consumer{ 18 | subConsumer: subConsumer, 19 | } 20 | } 21 | 22 | func (c Consumer) Register(m *consumers.Multiplexer, df *redsync.Redsync) error { 23 | return analyzesqueue.RegisterConsumer(c.consumeMessage, runQueueID, m, df) 24 | } 25 | 26 | func (c Consumer) consumeMessage(ctx context.Context, m *runMessage) error { 27 | return c.subConsumer.Consume(ctx, m.RepoName, m.AnalysisGUID, m.Branch, m.PrivateAccessToken, m.CommitSHA) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/worker/analyze/analyzesqueue/repoanalyzesqueue/producer.go: -------------------------------------------------------------------------------- 1 | package repoanalyzesqueue 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/queue/producers" 5 | ) 6 | 7 | type Producer struct { 8 | producers.Base 9 | } 10 | 11 | func (p *Producer) Register(m *producers.Multiplexer) error { 12 | return p.Base.Register(m, runQueueID) 13 | } 14 | 15 | func (p Producer) Put(repoName, analysisGUID, branch, privateAccessToken, commitSHA string) error { 16 | return p.Base.Put(runMessage{ 17 | RepoName: repoName, 18 | AnalysisGUID: analysisGUID, 19 | Branch: branch, 20 | PrivateAccessToken: privateAccessToken, 21 | CommitSHA: commitSHA, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/worker/analyze/linters/linter.go: -------------------------------------------------------------------------------- 1 | package linters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/pkg/goenvbuild/config" 7 | 8 | logresult "github.com/golangci/golangci-api/pkg/goenvbuild/result" 9 | "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 10 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 11 | ) 12 | 13 | //go:generate mockgen -package linters -source linter.go -destination linter_mock.go 14 | 15 | type Linter interface { 16 | Run(ctx context.Context, sg *logresult.StepGroup, exec executors.Executor, buildConfig *config.Service) (*result.Result, error) 17 | Name() string 18 | } 19 | -------------------------------------------------------------------------------- /pkg/worker/analyze/linters/linter_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: linter.go 3 | 4 | // Package linters is a generated GoMock package. 5 | package linters 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | config "github.com/golangci/golangci-api/pkg/goenvbuild/config" 11 | result "github.com/golangci/golangci-api/pkg/goenvbuild/result" 12 | result0 "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 13 | executors "github.com/golangci/golangci-api/pkg/worker/lib/executors" 14 | reflect "reflect" 15 | ) 16 | 17 | // MockLinter is a mock of Linter interface 18 | type MockLinter struct { 19 | ctrl *gomock.Controller 20 | recorder *MockLinterMockRecorder 21 | } 22 | 23 | // MockLinterMockRecorder is the mock recorder for MockLinter 24 | type MockLinterMockRecorder struct { 25 | mock *MockLinter 26 | } 27 | 28 | // NewMockLinter creates a new mock instance 29 | func NewMockLinter(ctrl *gomock.Controller) *MockLinter { 30 | mock := &MockLinter{ctrl: ctrl} 31 | mock.recorder = &MockLinterMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use 36 | func (m *MockLinter) EXPECT() *MockLinterMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // Run mocks base method 41 | func (m *MockLinter) Run(ctx context.Context, sg *result.StepGroup, exec executors.Executor, buildConfig *config.Service) (*result0.Result, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "Run", ctx, sg, exec, buildConfig) 44 | ret0, _ := ret[0].(*result0.Result) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // Run indicates an expected call of Run 50 | func (mr *MockLinterMockRecorder) Run(ctx, sg, exec, buildConfig interface{}) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockLinter)(nil).Run), ctx, sg, exec, buildConfig) 53 | } 54 | 55 | // Name mocks base method 56 | func (m *MockLinter) Name() string { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "Name") 59 | ret0, _ := ret[0].(string) 60 | return ret0 61 | } 62 | 63 | // Name indicates an expected call of Name 64 | func (mr *MockLinterMockRecorder) Name() *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockLinter)(nil).Name)) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/worker/analyze/linters/result/issue.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import golangciLintResult "github.com/golangci/golangci-lint/pkg/result" 4 | 5 | type Issue struct { 6 | FromLinter string 7 | Text string 8 | File string 9 | LineNumber int 10 | HunkPos int 11 | 12 | LineRange *golangciLintResult.Range 13 | Replacement *golangciLintResult.Replacement 14 | } 15 | 16 | func NewIssue(fromLinter, text, file string, lineNumber, hunkPos int) Issue { 17 | return Issue{ 18 | FromLinter: fromLinter, 19 | Text: text, 20 | File: file, 21 | LineNumber: lineNumber, 22 | HunkPos: hunkPos, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/worker/analyze/linters/result/result.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | type Result struct { 4 | Issues []Issue 5 | MaxIssuesPerFile int // Needed for gofmt and goimports where it is 1 6 | ResultJSON interface{} 7 | } 8 | -------------------------------------------------------------------------------- /pkg/worker/analyze/linters/runner.go: -------------------------------------------------------------------------------- 1 | package linters 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/golangci/golangci-api/pkg/goenvbuild/config" 8 | 9 | logresult "github.com/golangci/golangci-api/pkg/goenvbuild/result" 10 | "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 11 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 12 | ) 13 | 14 | type Runner interface { 15 | Run(ctx context.Context, sg *logresult.StepGroup, linters []Linter, exec executors.Executor, buildConfig *config.Service) (*result.Result, error) 16 | } 17 | 18 | type SimpleRunner struct { 19 | } 20 | 21 | func (r SimpleRunner) Run(ctx context.Context, sg *logresult.StepGroup, linters []Linter, exec executors.Executor, buildConfig *config.Service) (*result.Result, error) { 22 | results := []result.Result{} 23 | for _, linter := range linters { 24 | res, err := linter.Run(ctx, sg, exec, buildConfig) 25 | if err != nil { 26 | return nil, err // don't wrap error here, need to save original error 27 | } 28 | 29 | results = append(results, *res) 30 | } 31 | 32 | return r.mergeResults(results), nil 33 | } 34 | 35 | func (r SimpleRunner) mergeResults(results []result.Result) *result.Result { 36 | if len(results) == 0 { 37 | return nil 38 | } 39 | 40 | if len(results) > 1 { 41 | log.Fatalf("len(results) can't be more than 1: %+v", results) 42 | } 43 | 44 | // TODO: support for multiple linters, not only golangci-lint 45 | return &results[0] 46 | } 47 | -------------------------------------------------------------------------------- /pkg/worker/analyze/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 8 | ) 9 | 10 | type BuildLogger struct { 11 | buildLog *result.Log 12 | logger logutil.Log 13 | } 14 | 15 | func NewBuildLogger(buildLog *result.Log, logger logutil.Log) *BuildLogger { 16 | return &BuildLogger{ 17 | buildLog: buildLog, 18 | logger: logger, 19 | } 20 | } 21 | 22 | func (bl BuildLogger) lastStep() *result.Step { 23 | return bl.buildLog.LastStepGroup().LastStep() 24 | } 25 | 26 | func (bl BuildLogger) Fatalf(format string, args ...interface{}) { 27 | log.Fatalf(format, args...) 28 | } 29 | func (bl BuildLogger) Errorf(format string, args ...interface{}) { 30 | s := bl.lastStep() 31 | s.AddOutputLine("[ERROR] "+format, args...) 32 | bl.logger.Errorf(format, args...) 33 | } 34 | 35 | func (bl BuildLogger) Warnf(format string, args ...interface{}) { 36 | s := bl.lastStep() 37 | s.AddOutputLine("[WARN] "+format, args...) 38 | bl.logger.Warnf(format, args...) 39 | } 40 | func (bl BuildLogger) Infof(format string, args ...interface{}) { 41 | s := bl.lastStep() 42 | s.AddOutputLine(format, args...) 43 | } 44 | 45 | func (bl BuildLogger) Debugf(key string, format string, args ...interface{}) { 46 | } 47 | 48 | func (bl BuildLogger) Child(name string) logutil.Log { 49 | panic("child isn't supported") 50 | } 51 | 52 | func (bl BuildLogger) SetLevel(level logutil.LogLevel) { 53 | panic("setlevel isn't supported") 54 | } 55 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/def.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | const ( 4 | internalError = "Internal error" 5 | 6 | StatusSentToQueue = "sent_to_queue" 7 | StatusProcessing = "processing" 8 | StatusProcessed = "processed" 9 | StatusNotFound = "not_found" 10 | StatusError = "error" 11 | 12 | noGoFilesToAnalyzeMessage = "No Go files to analyze" 13 | noGoFilesToAnalyzeErr = "no go files to analyze" 14 | 15 | stepUpdateStatusToProcessing = `set analysis status to "processing"` 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/escape.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 8 | ) 9 | 10 | func escapeText(text string, secretableCtx secretable) string { 11 | secrets := buildSecrets(secretableCtx.secrets()...) 12 | 13 | ret := text 14 | for secret, replacement := range secrets { 15 | ret = strings.Replace(ret, secret, replacement, -1) 16 | } 17 | 18 | return ret 19 | } 20 | 21 | type secretable interface { 22 | secrets() []string 23 | } 24 | 25 | //nolint:gocyclo 26 | func buildSecrets(vars ...string) map[string]string { 27 | const minSecretValueLen = 6 28 | 29 | const hidden = "{hidden}" 30 | ret := map[string]string{} 31 | for _, v := range vars { 32 | if len(v) >= minSecretValueLen { 33 | ret[v] = hidden 34 | } 35 | } 36 | 37 | exclude := map[string]bool{ 38 | "APP_NAME": true, 39 | "ADMIN_GITHUB_LOGIN": true, 40 | "GITHUB_REVIEWER_LOGIN": true, 41 | "WEB_ROOT": true, 42 | "GOROOT": true, 43 | "GOPATH": true, 44 | "FARGATE_CONTAINER": true, 45 | "PATCH_STORE_DIR": true, 46 | } 47 | 48 | for _, kv := range os.Environ() { 49 | parts := strings.Split(kv, "=") 50 | if len(parts) != 2 { 51 | continue 52 | } 53 | 54 | k := parts[0] 55 | if exclude[k] { 56 | continue 57 | } 58 | 59 | if strings.HasSuffix(k, "_OWNERS") || strings.HasSuffix(k, "_PERCENT") || strings.HasSuffix(k, "_REPOS") { 60 | continue // experiment vars 61 | } 62 | 63 | v := parts[1] 64 | 65 | if strings.EqualFold(v, "golangci") || strings.EqualFold(v, "golangci-lint") { 66 | continue // just extra check because these are critical words 67 | } 68 | 69 | if len(v) >= minSecretValueLen { 70 | ret[v] = hidden 71 | } 72 | } 73 | 74 | return ret 75 | } 76 | 77 | func escapeBuildLog(buildLog *result.Log, s secretable) { 78 | for _, group := range buildLog.Groups { 79 | group.Name = escapeText(group.Name, s) 80 | for _, step := range group.Steps { 81 | step.Description = escapeText(step.Description, s) 82 | step.Error = escapeText(step.Error, s) 83 | for i := range step.OutputLines { 84 | step.OutputLines[i] = escapeText(step.OutputLines[i], s) 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/executor.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/session" 5 | "github.com/golangci/golangci-api/internal/shared/config" 6 | "github.com/golangci/golangci-api/pkg/worker/lib/experiments" 7 | "github.com/golangci/golangci-api/pkg/worker/lib/github" 8 | 9 | "github.com/golangci/golangci-api/internal/shared/logutil" 10 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func makeExecutor(log logutil.Log, ec *experiments.Checker, 15 | repo *github.Repo, cfg config.Config, awsSess *session.Session, isPull bool) (executors.Executor, error) { 16 | 17 | if ec.IsActiveForAnalysis("FARGATE_EXECUTOR", repo, isPull) { 18 | return executors.NewFargate(log, cfg, awsSess).WithWorkDir("/goapp"), nil 19 | } 20 | 21 | ce, err := executors.NewContainer(log) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "can't build container executor") 24 | } 25 | return ce.WithWorkDir("/goapp"), nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/nop_processor.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | import "context" 4 | 5 | type NopProcessor struct{} 6 | 7 | func (p NopProcessor) Process(ctx context.Context) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/pull_processor.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/pkg/goenvbuild/config" 7 | 8 | "github.com/golangci/golangci-api/internal/shared/logutil" 9 | "github.com/golangci/golangci-api/pkg/worker/lib/github" 10 | gh "github.com/google/go-github/github" 11 | ) 12 | 13 | type PullProcessor interface { 14 | Process(ctx *PullContext) error 15 | } 16 | 17 | type PullContext struct { 18 | Ctx context.Context 19 | UserID int 20 | AnalysisGUID string 21 | CommitSHA string 22 | ProviderCtx *github.Context 23 | LogCtx logutil.Context 24 | Log logutil.Log 25 | 26 | pull *gh.PullRequest 27 | 28 | res *analysisResult 29 | savedLog logutil.Log 30 | buildConfig *config.Service 31 | } 32 | 33 | func (ctx *PullContext) repo() *github.Repo { 34 | return &ctx.ProviderCtx.Repo 35 | } 36 | 37 | func (ctx *PullContext) secrets() []string { 38 | return []string{ctx.ProviderCtx.GithubAccessToken, ctx.AnalysisGUID} 39 | } 40 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/pull_processor_factory.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | type PullProcessorFactory interface { 4 | BuildProcessor(ctx *PullContext) (PullProcessor, func(), error) 5 | } 6 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/result.go: -------------------------------------------------------------------------------- 1 | package processors 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 8 | lintersResult "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 9 | ) 10 | 11 | type analysisResult struct { 12 | resultCollector 13 | buildLog *result.Log 14 | lintRes *lintersResult.Result 15 | } 16 | 17 | type JSONDuration time.Duration 18 | 19 | func (d JSONDuration) MarshalJSON() ([]byte, error) { 20 | return []byte(strconv.Itoa(int(time.Duration(d) / time.Millisecond))), nil 21 | } 22 | 23 | func (d JSONDuration) String() string { 24 | return time.Duration(d).String() 25 | } 26 | 27 | type Timing struct { 28 | Name string 29 | Duration JSONDuration `json:"DurationMs"` 30 | } 31 | 32 | type Warning struct { 33 | Tag string 34 | Text string 35 | } 36 | 37 | type resultCollector struct { 38 | timings []Timing 39 | warnings []Warning 40 | } 41 | 42 | func (r *resultCollector) trackTiming(name string, f func()) { 43 | startedAt := time.Now() 44 | f() 45 | r.timings = append(r.timings, Timing{ 46 | Name: name, 47 | Duration: JSONDuration(time.Since(startedAt)), 48 | }) 49 | } 50 | 51 | func (r *resultCollector) addTimingFrom(name string, from time.Time) { 52 | r.timings = append(r.timings, Timing{ 53 | Name: name, 54 | Duration: JSONDuration(time.Since(from)), 55 | }) 56 | } 57 | 58 | func (r *resultCollector) publicWarn(tag string, text string) { 59 | r.warnings = append(r.warnings, Warning{ 60 | Tag: tag, 61 | Text: text, 62 | }) 63 | } 64 | 65 | type workerRes struct { 66 | Timings []Timing `json:",omitempty"` 67 | Warnings []Warning `json:",omitempty"` 68 | Error string `json:",omitempty"` 69 | } 70 | 71 | type resultJSON struct { 72 | Version int 73 | GolangciLintRes interface{} 74 | WorkerRes workerRes 75 | BuildLog *result.Log 76 | } 77 | 78 | func fromDBTime(t time.Time) time.Time { 79 | return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.Local) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/test/1.patch: -------------------------------------------------------------------------------- 1 | --- a/main.go 2 | +++ a/main.go 3 | @@ -1,5 +1,11 @@ 4 | package p 5 | 6 | -func F0() error { 7 | +import "fmt" 8 | + 9 | +func F0New() error { 10 | return nil 11 | } 12 | + 13 | +func F1() error { 14 | + return fmt.Errorf("error") 15 | +} 16 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/test/main0.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | func F0() error { 4 | return nil 5 | } 6 | -------------------------------------------------------------------------------- /pkg/worker/analyze/processors/test/main1.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | import "fmt" 4 | 5 | func F0New() error { 6 | return nil 7 | } 8 | 9 | func F1() error { 10 | return fmt.Errorf("error") 11 | } 12 | -------------------------------------------------------------------------------- /pkg/worker/analyze/prstate/api_storage.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl 2 | package prstate 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/golangci/golangci-api/pkg/worker/lib/httputils" 11 | ) 12 | 13 | type APIStorage struct { 14 | host string 15 | client httputils.Client 16 | } 17 | 18 | func NewAPIStorage(client httputils.Client) *APIStorage { 19 | return &APIStorage{ 20 | client: client, 21 | host: os.Getenv("API_URL"), 22 | } 23 | } 24 | 25 | func (s APIStorage) getStatusURL(owner, name, analysisID string) string { 26 | return fmt.Sprintf("%s/v1/repos/github.com/%s/%s/analyzes/%s/state", s.host, owner, name, analysisID) 27 | } 28 | 29 | func (s APIStorage) UpdateState(ctx context.Context, owner, name, analysisID string, state *State) error { 30 | return s.client.Put(ctx, s.getStatusURL(owner, name, analysisID), state) 31 | } 32 | 33 | func (s APIStorage) GetState(ctx context.Context, owner, name, analysisID string) (*State, error) { 34 | bodyReader, err := s.client.Get(ctx, s.getStatusURL(owner, name, analysisID)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | defer bodyReader.Close() 40 | 41 | var state State 42 | if err = json.NewDecoder(bodyReader).Decode(&state); err != nil { 43 | return nil, fmt.Errorf("can't read json body: %s", err) 44 | } 45 | 46 | return &state, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/worker/analyze/prstate/storage.go: -------------------------------------------------------------------------------- 1 | package prstate 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | //go:generate mockgen -package prstate -source storage.go -destination storage_mock.go 9 | 10 | type State struct { 11 | CreatedAt time.Time 12 | Status string 13 | ReportedIssuesCount int 14 | ResultJSON interface{} 15 | } 16 | 17 | type Storage interface { 18 | UpdateState(ctx context.Context, owner, name, analysisID string, state *State) error 19 | GetState(ctx context.Context, owner, name, analysisID string) (*State, error) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/worker/analyze/prstate/storage_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: storage.go 3 | 4 | // Package prstate is a generated GoMock package. 5 | package prstate 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockStorage is a mock of Storage interface 14 | type MockStorage struct { 15 | ctrl *gomock.Controller 16 | recorder *MockStorageMockRecorder 17 | } 18 | 19 | // MockStorageMockRecorder is the mock recorder for MockStorage 20 | type MockStorageMockRecorder struct { 21 | mock *MockStorage 22 | } 23 | 24 | // NewMockStorage creates a new mock instance 25 | func NewMockStorage(ctrl *gomock.Controller) *MockStorage { 26 | mock := &MockStorage{ctrl: ctrl} 27 | mock.recorder = &MockStorageMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockStorage) EXPECT() *MockStorageMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // UpdateState mocks base method 37 | func (m *MockStorage) UpdateState(ctx context.Context, owner, name, analysisID string, state *State) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "UpdateState", ctx, owner, name, analysisID, state) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // UpdateState indicates an expected call of UpdateState 45 | func (mr *MockStorageMockRecorder) UpdateState(ctx, owner, name, analysisID, state interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateState", reflect.TypeOf((*MockStorage)(nil).UpdateState), ctx, owner, name, analysisID, state) 48 | } 49 | 50 | // GetState mocks base method 51 | func (m *MockStorage) GetState(ctx context.Context, owner, name, analysisID string) (*State, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "GetState", ctx, owner, name, analysisID) 54 | ret0, _ := ret[0].(*State) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // GetState indicates an expected call of GetState 60 | func (mr *MockStorageMockRecorder) GetState(ctx, owner, name, analysisID interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetState", reflect.TypeOf((*MockStorage)(nil).GetState), ctx, owner, name, analysisID) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/worker/analyze/reporters/reporter.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/pkg/goenvbuild/config" 7 | 8 | envbuildresult "github.com/golangci/golangci-api/pkg/goenvbuild/result" 9 | "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 10 | ) 11 | 12 | //go:generate mockgen -package reporters -source reporter.go -destination reporter_mock.go 13 | 14 | type Reporter interface { 15 | Report(ctx context.Context, buildConfig *config.Service, buildLog *envbuildresult.Log, ref string, issues []result.Issue) error 16 | } 17 | -------------------------------------------------------------------------------- /pkg/worker/analyze/reporters/reporter_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: reporter.go 3 | 4 | // Package reporters is a generated GoMock package. 5 | package reporters 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | config "github.com/golangci/golangci-api/pkg/goenvbuild/config" 11 | result "github.com/golangci/golangci-api/pkg/goenvbuild/result" 12 | result0 "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 13 | reflect "reflect" 14 | ) 15 | 16 | // MockReporter is a mock of Reporter interface 17 | type MockReporter struct { 18 | ctrl *gomock.Controller 19 | recorder *MockReporterMockRecorder 20 | } 21 | 22 | // MockReporterMockRecorder is the mock recorder for MockReporter 23 | type MockReporterMockRecorder struct { 24 | mock *MockReporter 25 | } 26 | 27 | // NewMockReporter creates a new mock instance 28 | func NewMockReporter(ctrl *gomock.Controller) *MockReporter { 29 | mock := &MockReporter{ctrl: ctrl} 30 | mock.recorder = &MockReporterMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use 35 | func (m *MockReporter) EXPECT() *MockReporterMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Report mocks base method 40 | func (m *MockReporter) Report(ctx context.Context, buildConfig *config.Service, buildLog *result.Log, ref string, issues []result0.Issue) error { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Report", ctx, buildConfig, buildLog, ref, issues) 43 | ret0, _ := ret[0].(error) 44 | return ret0 45 | } 46 | 47 | // Report indicates an expected call of Report 48 | func (mr *MockReporterMockRecorder) Report(ctx, buildConfig, buildLog, ref, issues interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Report", reflect.TypeOf((*MockReporter)(nil).Report), ctx, buildConfig, buildLog, ref, issues) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/worker/analyze/repostate/api_storage.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl 2 | package repostate 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/golangci/golangci-api/pkg/worker/lib/httputils" 11 | ) 12 | 13 | type APIStorage struct { 14 | host string 15 | client httputils.Client 16 | } 17 | 18 | func NewAPIStorage(client httputils.Client) *APIStorage { 19 | return &APIStorage{ 20 | client: client, 21 | host: os.Getenv("API_URL"), 22 | } 23 | } 24 | 25 | func (s APIStorage) getAnalysisURL(owner, name, analysisID string) string { 26 | return fmt.Sprintf("%s/v1/repos/github.com/%s/%s/repoanalyzes/%s", s.host, owner, name, analysisID) 27 | } 28 | 29 | func (s APIStorage) UpdateState(ctx context.Context, owner, name, analysisID string, state *State) error { 30 | return s.client.Put(ctx, s.getAnalysisURL(owner, name, analysisID), state) 31 | } 32 | 33 | func (s APIStorage) GetState(ctx context.Context, owner, name, analysisID string) (*State, error) { 34 | bodyReader, err := s.client.Get(ctx, s.getAnalysisURL(owner, name, analysisID)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | defer bodyReader.Close() 40 | 41 | var state State 42 | if err = json.NewDecoder(bodyReader).Decode(&state); err != nil { 43 | return nil, fmt.Errorf("can't read json body: %s", err) 44 | } 45 | 46 | return &state, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/worker/analyze/repostate/storage.go: -------------------------------------------------------------------------------- 1 | package repostate 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | //go:generate mockgen -package repostate -source storage.go -destination storage_mock.go 9 | 10 | type State struct { 11 | CreatedAt time.Time 12 | Status string 13 | ResultJSON interface{} 14 | } 15 | 16 | type Storage interface { 17 | UpdateState(ctx context.Context, owner, name, analysisID string, state *State) error 18 | GetState(ctx context.Context, owner, name, analysisID string) (*State, error) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/worker/analyze/repostate/storage_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: storage.go 3 | 4 | // Package repostate is a generated GoMock package. 5 | package repostate 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockStorage is a mock of Storage interface 14 | type MockStorage struct { 15 | ctrl *gomock.Controller 16 | recorder *MockStorageMockRecorder 17 | } 18 | 19 | // MockStorageMockRecorder is the mock recorder for MockStorage 20 | type MockStorageMockRecorder struct { 21 | mock *MockStorage 22 | } 23 | 24 | // NewMockStorage creates a new mock instance 25 | func NewMockStorage(ctrl *gomock.Controller) *MockStorage { 26 | mock := &MockStorage{ctrl: ctrl} 27 | mock.recorder = &MockStorageMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockStorage) EXPECT() *MockStorageMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // UpdateState mocks base method 37 | func (m *MockStorage) UpdateState(ctx context.Context, owner, name, analysisID string, state *State) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "UpdateState", ctx, owner, name, analysisID, state) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // UpdateState indicates an expected call of UpdateState 45 | func (mr *MockStorageMockRecorder) UpdateState(ctx, owner, name, analysisID, state interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateState", reflect.TypeOf((*MockStorage)(nil).UpdateState), ctx, owner, name, analysisID, state) 48 | } 49 | 50 | // GetState mocks base method 51 | func (m *MockStorage) GetState(ctx context.Context, owner, name, analysisID string) (*State, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "GetState", ctx, owner, name, analysisID) 54 | ret0, _ := ret[0].(*State) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // GetState indicates an expected call of GetState 60 | func (mr *MockStorageMockRecorder) GetState(ctx, owner, name, analysisID interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetState", reflect.TypeOf((*MockStorage)(nil).GetState), ctx, owner, name, analysisID) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/worker/analyze/resources/requirements.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 5 | "github.com/golangci/golangci-api/pkg/worker/lib/experiments" 6 | "github.com/golangci/golangci-api/pkg/worker/lib/github" 7 | ) 8 | 9 | func BuildExecutorRequirementsForRepo(ec *experiments.Checker, repo *github.Repo) *executors.Requirements { 10 | maxRequirements := executors.Requirements{ 11 | CPUCount: 4, 12 | MemoryGB: 30, 13 | } 14 | if repo.IsPrivate { 15 | return &maxRequirements 16 | } 17 | 18 | if ec.IsActiveForRepo("MAX_RESOURCE_REQUIREMENTS", repo.Owner, repo.Name) { 19 | return &maxRequirements 20 | } 21 | 22 | return &executors.Requirements{ 23 | CPUCount: 4, 24 | MemoryGB: 16, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/worker/app/app_modifiers.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/pkg/worker/analyze/processors" 5 | ) 6 | 7 | type Modifier func(a *App) 8 | 9 | func SetPullProcessorFactory(pf processors.PullProcessorFactory) Modifier { 10 | return func(a *App) { 11 | a.ppf = pf 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/worker/app/app_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/golangci/golangci-api/pkg/worker/analyze/analyzesqueue/pullanalyzesqueue" 8 | 9 | "github.com/golangci/golangci-api/pkg/worker/analyze/processors" 10 | "github.com/golangci/golangci-api/pkg/worker/test" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type testProcessor struct { 15 | notifyCh chan bool 16 | } 17 | 18 | func (tp testProcessor) Process(*processors.PullContext) error { 19 | tp.notifyCh <- true 20 | return nil 21 | } 22 | 23 | type testProcessorFatory struct { 24 | t *testing.T 25 | expAnalysisGUID string 26 | notifyCh chan bool 27 | } 28 | 29 | func (tpf testProcessorFatory) BuildProcessor(ctx *processors.PullContext) (processors.PullProcessor, func(), error) { 30 | assert.Equal(tpf.t, tpf.expAnalysisGUID, ctx.AnalysisGUID) 31 | return testProcessor{ 32 | notifyCh: tpf.notifyCh, 33 | }, nil, nil 34 | } 35 | 36 | func TestSendReceiveProcessing(t *testing.T) { 37 | notifyCh := make(chan bool) 38 | testGUID := "test" 39 | pf := testProcessorFatory{ 40 | t: t, 41 | expAnalysisGUID: testGUID, 42 | notifyCh: notifyCh, 43 | } 44 | 45 | test.Init() 46 | a := NewApp(SetPullProcessorFactory(pf)) 47 | go a.Run() 48 | 49 | testDeps := a.BuildTestDeps() 50 | msg := pullanalyzesqueue.RunMessage{ 51 | AnalysisGUID: testGUID, 52 | } 53 | assert.NoError(t, testDeps.PullAnalyzesRunner.Put(&msg)) 54 | 55 | select { 56 | case <-notifyCh: 57 | return 58 | case <-time.After(time.Second * 3): 59 | t.Fatalf("Timeouted waiting of processing") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/worker/lib/errorutils/errors.go: -------------------------------------------------------------------------------- 1 | package errorutils 2 | 3 | type InternalError struct { 4 | PublicDesc string 5 | PrivateDesc string 6 | StdErr string 7 | IsPermanent bool 8 | } 9 | 10 | func (e InternalError) Error() string { 11 | return e.PrivateDesc 12 | } 13 | 14 | type BadInputError struct { 15 | PublicDesc string 16 | } 17 | 18 | func (e BadInputError) Error() string { 19 | return e.PublicDesc 20 | } 21 | -------------------------------------------------------------------------------- /pkg/worker/lib/executors/env.go: -------------------------------------------------------------------------------- 1 | package executors 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type envStore struct { 9 | env []string 10 | } 11 | 12 | func newEnvStore() *envStore { 13 | return &envStore{ 14 | env: os.Environ(), 15 | } 16 | } 17 | 18 | func (e *envStore) SetEnv(k, v string) { 19 | e.env = append(e.env, fmt.Sprintf("%s=%s", k, v)) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/worker/lib/executors/executor.go: -------------------------------------------------------------------------------- 1 | package executors 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | //go:generate mockgen -package executors -source executor.go -destination executor_mock.go 10 | 11 | type RunResult struct { 12 | StdOut string 13 | StdErr string 14 | } 15 | 16 | var ErrExecutorFail = errors.New("executor failed") 17 | 18 | type Requirements struct { 19 | CPUCount int 20 | MemoryGB int 21 | } 22 | 23 | type Executor interface { 24 | Setup(ctx context.Context, req *Requirements) error 25 | Run(ctx context.Context, name string, args ...string) (*RunResult, error) 26 | 27 | WithEnv(k, v string) Executor 28 | SetEnv(k, v string) 29 | 30 | WorkDir() string 31 | WithWorkDir(wd string) Executor 32 | 33 | CopyFile(ctx context.Context, dst, src string) error 34 | 35 | Clean() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/worker/lib/executors/temp_dir_shell.go: -------------------------------------------------------------------------------- 1 | package executors 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/golangci/golangci-api/pkg/worker/analytics" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type TempDirShell struct { 16 | shell 17 | } 18 | 19 | var _ Executor = &TempDirShell{} 20 | 21 | func NewTempDirShell(tag string) (*TempDirShell, error) { 22 | tmpRoot, err := filepath.EvalSymlinks("/tmp") 23 | if err != nil { 24 | return nil, errors.Wrap(err, "can't eval symlinks on /tmp") 25 | } 26 | 27 | wd, err := ioutil.TempDir(tmpRoot, fmt.Sprintf("golangci.%s", tag)) 28 | if err != nil { 29 | return nil, errors.Wrap(err, "can't make temp dir") 30 | } 31 | 32 | return &TempDirShell{ 33 | shell: *newShell(wd), 34 | }, nil 35 | } 36 | 37 | func (s TempDirShell) WorkDir() string { 38 | return s.wd 39 | } 40 | 41 | func (s *TempDirShell) SetWorkDir(wd string) { 42 | s.wd = wd 43 | } 44 | 45 | func (s TempDirShell) Clean() { 46 | if err := os.RemoveAll(s.wd); err != nil { 47 | analytics.Log(context.TODO()).Warnf("Can't remove temp dir %s: %s", s.wd, err) 48 | } 49 | } 50 | 51 | func (s TempDirShell) WithEnv(k, v string) Executor { 52 | eCopy := s 53 | eCopy.SetEnv(k, v) 54 | return &eCopy 55 | } 56 | 57 | func (s TempDirShell) WithWorkDir(wd string) Executor { 58 | eCopy := s 59 | eCopy.wd = wd 60 | return &eCopy 61 | } 62 | 63 | func (s TempDirShell) CopyFile(ctx context.Context, dst, src string) error { 64 | dst = filepath.Join(s.WorkDir(), dst) 65 | 66 | from, err := os.Open(src) 67 | if err != nil { 68 | return fmt.Errorf("can't open %s: %s", src, err) 69 | } 70 | defer from.Close() 71 | 72 | to, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0666) 73 | if err != nil { 74 | return fmt.Errorf("can't open %s: %s", dst, err) 75 | } 76 | defer to.Close() 77 | 78 | _, err = io.Copy(to, from) 79 | if err != nil { 80 | return fmt.Errorf("can't copy from %s to %s: %s", src, dst, err) 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/worker/lib/executors/temp_dir_shell_test.go: -------------------------------------------------------------------------------- 1 | package executors 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTempDirShellWithEnv(t *testing.T) { 11 | ts, err := NewTempDirShell(t.Name()) 12 | assert.NoError(t, err) 13 | assert.NotEmpty(t, ts.wd) 14 | assert.Equal(t, os.Environ(), ts.env) 15 | 16 | defer ts.Clean() 17 | 18 | tse := ts.WithEnv("k", "v").(*TempDirShell) 19 | assert.NotEmpty(t, ts.wd) 20 | assert.Equal(t, ts.wd, tse.wd) // check was saved 21 | 22 | assert.Equal(t, os.Environ(), ts.env) // check didn't change 23 | assert.Equal(t, append(os.Environ(), "k=v"), tse.env) 24 | } 25 | 26 | func exists(t *testing.T, path string) bool { 27 | _, err := os.Stat(path) 28 | if err == nil { 29 | return true 30 | } 31 | 32 | if os.IsNotExist(err) { 33 | return false 34 | } 35 | 36 | assert.NoError(t, err) 37 | return true 38 | } 39 | 40 | func TestTempDirShellClean(t *testing.T) { 41 | ts, err := NewTempDirShell(t.Name()) 42 | assert.NoError(t, err) 43 | 44 | assert.True(t, exists(t, ts.WorkDir())) 45 | ts.Clean() 46 | assert.False(t, exists(t, ts.WorkDir())) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/worker/lib/fetchers/fetcher.go: -------------------------------------------------------------------------------- 1 | package fetchers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 7 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 8 | ) 9 | 10 | //go:generate mockgen -package fetchers -source fetcher.go -destination fetcher_mock.go 11 | 12 | type Fetcher interface { 13 | Fetch(ctx context.Context, sg *result.StepGroup, repo *Repo, exec executors.Executor) error 14 | } 15 | -------------------------------------------------------------------------------- /pkg/worker/lib/fetchers/fetcher_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: fetcher.go 3 | 4 | // Package fetchers is a generated GoMock package. 5 | package fetchers 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | result "github.com/golangci/golangci-api/pkg/goenvbuild/result" 11 | executors "github.com/golangci/golangci-api/pkg/worker/lib/executors" 12 | reflect "reflect" 13 | ) 14 | 15 | // MockFetcher is a mock of Fetcher interface 16 | type MockFetcher struct { 17 | ctrl *gomock.Controller 18 | recorder *MockFetcherMockRecorder 19 | } 20 | 21 | // MockFetcherMockRecorder is the mock recorder for MockFetcher 22 | type MockFetcherMockRecorder struct { 23 | mock *MockFetcher 24 | } 25 | 26 | // NewMockFetcher creates a new mock instance 27 | func NewMockFetcher(ctrl *gomock.Controller) *MockFetcher { 28 | mock := &MockFetcher{ctrl: ctrl} 29 | mock.recorder = &MockFetcherMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use 34 | func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Fetch mocks base method 39 | func (m *MockFetcher) Fetch(ctx context.Context, sg *result.StepGroup, repo *Repo, exec executors.Executor) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Fetch", ctx, sg, repo, exec) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Fetch indicates an expected call of Fetch 47 | func (mr *MockFetcherMockRecorder) Fetch(ctx, sg, repo, exec interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockFetcher)(nil).Fetch), ctx, sg, repo, exec) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/worker/lib/fetchers/git_test.go: -------------------------------------------------------------------------------- 1 | package fetchers 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 9 | 10 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestGitOnTestRepo(t *testing.T) { 15 | exec, err := executors.NewTempDirShell("test.git") 16 | assert.NoError(t, err) 17 | defer exec.Clean() 18 | g := NewGit() 19 | 20 | repo := &Repo{ 21 | Ref: "test-branch", 22 | CloneURL: "git@github.com:golangci/test.git", 23 | } 24 | 25 | sg := &result.StepGroup{} 26 | err = g.Fetch(context.Background(), sg, repo, exec) 27 | assert.NoError(t, err) 28 | 29 | files, err := ioutil.ReadDir(exec.WorkDir()) 30 | assert.NoError(t, err) 31 | assert.Len(t, files, 3) 32 | assert.Equal(t, ".git", files[0].Name()) 33 | assert.Equal(t, "README.md", files[1].Name()) 34 | assert.Equal(t, "main.go", files[2].Name()) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/worker/lib/fetchers/repo.go: -------------------------------------------------------------------------------- 1 | package fetchers 2 | 3 | type Repo struct { 4 | CloneURL string 5 | Ref string 6 | CommitSHA string 7 | FullPath string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/worker/lib/github/context.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/google/go-github/github" 9 | gh "github.com/google/go-github/github" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | type Repo struct { 14 | Owner, Name string 15 | IsPrivate bool 16 | } 17 | 18 | func (r Repo) FullName() string { 19 | return fmt.Sprintf("%s/%s", r.Owner, r.Name) 20 | } 21 | 22 | type Context struct { 23 | Repo Repo 24 | GithubAccessToken string 25 | PullRequestNumber int 26 | } 27 | 28 | func (c Context) GetHTTPClient(ctx context.Context) *http.Client { 29 | ts := oauth2.StaticTokenSource( 30 | &oauth2.Token{AccessToken: c.GithubAccessToken}, 31 | ) 32 | return oauth2.NewClient(ctx, ts) 33 | } 34 | 35 | func (c Context) GetClient(ctx context.Context) *github.Client { 36 | return github.NewClient(c.GetHTTPClient(ctx)) 37 | } 38 | 39 | func (c Context) GetCloneURL(repo *gh.Repository) string { 40 | if repo.GetPrivate() { 41 | return fmt.Sprintf("https://%s@github.com/%s/%s.git", 42 | c.GithubAccessToken, // it's already the private token 43 | c.Repo.Owner, c.Repo.Name) 44 | } 45 | 46 | return repo.GetCloneURL() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/worker/lib/goutils/environments/environment.go: -------------------------------------------------------------------------------- 1 | package environments 2 | 3 | type EnvSettable interface { 4 | SetEnv(key, value string) 5 | } 6 | -------------------------------------------------------------------------------- /pkg/worker/lib/goutils/environments/golang.go: -------------------------------------------------------------------------------- 1 | package environments 2 | 3 | type Golang struct { 4 | gopath string 5 | } 6 | 7 | func NewGolang(gopath string) *Golang { 8 | return &Golang{ 9 | gopath: gopath, 10 | } 11 | } 12 | 13 | func (g Golang) Setup(es EnvSettable) { 14 | es.SetEnv("GOPATH", g.gopath) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/worker/lib/goutils/workspaces/workspace.go: -------------------------------------------------------------------------------- 1 | package workspaces 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golangci/golangci-api/pkg/goenvbuild/config" 7 | 8 | "github.com/golangci/golangci-api/pkg/goenvbuild/result" 9 | "github.com/golangci/golangci-api/pkg/worker/lib/executors" 10 | "github.com/golangci/golangci-api/pkg/worker/lib/fetchers" 11 | ) 12 | 13 | type Installer interface { 14 | Setup(ctx context.Context, buildLog *result.Log, privateAccessToken string, 15 | repo *fetchers.Repo, projectPathParts ...string) (executors.Executor, *config.Service, error) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/worker/lib/httputils/client.go: -------------------------------------------------------------------------------- 1 | package httputils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/golangci/golangci-api/pkg/worker/analytics" 9 | "github.com/levigross/grequests" 10 | ) 11 | 12 | //go:generate mockgen -package httputils -source client.go -destination client_mock.go 13 | 14 | type Client interface { 15 | Get(ctx context.Context, url string) (io.ReadCloser, error) 16 | Put(ctx context.Context, url string, jsonObj interface{}) error 17 | } 18 | 19 | type GrequestsClient struct { 20 | header map[string]string 21 | } 22 | 23 | func NewGrequestsClient(header map[string]string) *GrequestsClient { 24 | return &GrequestsClient{ 25 | header: header, 26 | } 27 | } 28 | 29 | func (c GrequestsClient) Get(ctx context.Context, url string) (io.ReadCloser, error) { 30 | resp, err := grequests.Get(url, &grequests.RequestOptions{ 31 | Context: ctx, 32 | Headers: c.header, 33 | }) 34 | if err != nil { 35 | return nil, fmt.Errorf("unable to make GET http request %q: %s", url, err) 36 | } 37 | 38 | if !resp.Ok { 39 | if cerr := resp.Close(); cerr != nil { 40 | analytics.Log(ctx).Warnf("Can't close %q response: %s", url, cerr) 41 | } 42 | 43 | return nil, fmt.Errorf("got error code from %q: %d", url, resp.StatusCode) 44 | } 45 | 46 | return resp, nil 47 | } 48 | 49 | func (c GrequestsClient) Put(ctx context.Context, url string, jsonObj interface{}) error { 50 | resp, err := grequests.Put(url, &grequests.RequestOptions{ 51 | Context: ctx, 52 | Headers: c.header, 53 | JSON: jsonObj, 54 | }) 55 | if err != nil { 56 | return fmt.Errorf("unable to make PUT http request %q: %s", url, err) 57 | } 58 | 59 | if !resp.Ok { 60 | if cerr := resp.Close(); cerr != nil { 61 | analytics.Log(ctx).Warnf("Can't close %q response: %s", url, cerr) 62 | } 63 | 64 | return fmt.Errorf("got error code from %q: %d", url, resp.StatusCode) 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/worker/lib/httputils/client_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: client.go 3 | 4 | // Package httputils is a generated GoMock package. 5 | package httputils 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | io "io" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockClient is a mock of Client interface 15 | type MockClient struct { 16 | ctrl *gomock.Controller 17 | recorder *MockClientMockRecorder 18 | } 19 | 20 | // MockClientMockRecorder is the mock recorder for MockClient 21 | type MockClientMockRecorder struct { 22 | mock *MockClient 23 | } 24 | 25 | // NewMockClient creates a new mock instance 26 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 27 | mock := &MockClient{ctrl: ctrl} 28 | mock.recorder = &MockClientMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Get mocks base method 38 | func (m *MockClient) Get(ctx context.Context, url string) (io.ReadCloser, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Get", ctx, url) 41 | ret0, _ := ret[0].(io.ReadCloser) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Get indicates an expected call of Get 47 | func (mr *MockClientMockRecorder) Get(ctx, url interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), ctx, url) 50 | } 51 | 52 | // Put mocks base method 53 | func (m *MockClient) Put(ctx context.Context, url string, jsonObj interface{}) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "Put", ctx, url, jsonObj) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // Put indicates an expected call of Put 61 | func (mr *MockClientMockRecorder) Put(ctx, url, jsonObj interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockClient)(nil).Put), ctx, url, jsonObj) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/worker/lib/runmode/runmode.go: -------------------------------------------------------------------------------- 1 | package runmode 2 | 3 | import "os" 4 | 5 | func IsProduction() bool { 6 | return os.Getenv("GO_ENV") == "prod" 7 | } 8 | 9 | func IsDebug() bool { 10 | return os.Getenv("DEBUG") == "1" 11 | } 12 | -------------------------------------------------------------------------------- /pkg/worker/lib/timeutils/track.go: -------------------------------------------------------------------------------- 1 | package timeutils 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func Track(now time.Time, format string, args ...interface{}) { 11 | logrus.Infof("[timing] %s took %s", fmt.Sprintf(format, args...), time.Since(now)) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/worker/scripts/cleanup.sh: -------------------------------------------------------------------------------- 1 | go clean -cache 2 | rm -rf /tmp/glide-vendor* 3 | rm -rf /tmp/go-build* 4 | rm -rf $HOME/.glide/cache -------------------------------------------------------------------------------- /pkg/worker/test/env.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/golangci/golangci-api/internal/shared/fsutil" 11 | "github.com/joho/godotenv" 12 | ) 13 | 14 | var initOnce sync.Once 15 | 16 | func LoadEnv() { 17 | envNames := []string{".env"} 18 | for _, envName := range envNames { 19 | fpath := path.Join(fsutil.GetProjectRoot(), envName) 20 | err := godotenv.Overload(fpath) 21 | if err != nil { 22 | log.Fatalf("Can't load %s: %s", envName, err) 23 | } 24 | } 25 | } 26 | 27 | func Init() { 28 | initOnce.Do(func() { 29 | LoadEnv() 30 | }) 31 | } 32 | 33 | func MarkAsSlow(t *testing.T) { 34 | if os.Getenv("SLOW_TESTS_ENABLED") != "1" { 35 | t.SkipNow() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/worker/test/linters.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/pkg/worker/analyze/linters/result" 5 | ) 6 | 7 | func NewIssue(linter, message string, line int) result.Issue { 8 | return result.Issue{ 9 | FromLinter: linter, 10 | Text: message, 11 | File: "p/f.go", 12 | LineNumber: line, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/build_email_campaign/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "github.com/golangci/golangci-api/internal/shared/config" 11 | "github.com/golangci/golangci-api/internal/shared/logutil" 12 | 13 | "github.com/golangci/golangci-api/internal/shared/db/gormdb" 14 | "github.com/golangci/golangci-api/pkg/api/models" 15 | _ "github.com/joho/godotenv/autoload" 16 | _ "github.com/lib/pq" 17 | ) 18 | 19 | func main() { 20 | if err := buildUsersList(); err != nil { 21 | log.Fatalf("Failed to build users list: %s", err) 22 | } 23 | log.Printf("Successfully build users list") 24 | } 25 | 26 | func buildUsersList() error { 27 | log := logutil.NewStderrLog("") 28 | cfg := config.NewEnvConfig(log) 29 | db, err := gormdb.GetDB(cfg, log, "") 30 | if err != nil { 31 | return errors.Wrap(err, "failed to get gorm db") 32 | } 33 | 34 | var users []models.User 35 | if err = models.NewUserQuerySet(db).All(&users); err != nil { 36 | return errors.Wrap(err, "failed to get users") 37 | } 38 | 39 | lines := []string{"email,"} 40 | seenEmails := map[string]bool{} 41 | for _, u := range users { 42 | email := strings.ToLower(u.Email) 43 | if !strings.Contains(email, "@") { 44 | continue 45 | } 46 | 47 | if seenEmails[email] { 48 | continue 49 | } 50 | seenEmails[email] = true 51 | 52 | lines = append(lines, email) 53 | } 54 | 55 | fmt.Println(strings.Join(lines, "\n")) 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /scripts/consume_dlq/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import app "github.com/golangci/golangci-api/pkg/api" 4 | 5 | func main() { 6 | a := app.NewApp() 7 | a.RunDeadLetterConsumers() 8 | } 9 | -------------------------------------------------------------------------------- /scripts/decode_cookie/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | "github.com/gorilla/securecookie" 9 | ) 10 | 11 | func main() { 12 | value := flag.String("value", "", "cookie/session value") 13 | encode := flag.Bool("encode", true, "Encode/Decode") 14 | flag.Parse() 15 | 16 | sessSecret := os.Getenv("SESSION_SECRET") 17 | if sessSecret == "" { 18 | panic("SESSION_SECRET isn't set") 19 | } 20 | 21 | codecs := securecookie.CodecsFromPairs([]byte(sessSecret)) 22 | if *encode { 23 | encodedValue, err := securecookie.EncodeMulti("s", *value, codecs...) 24 | if err != nil { 25 | log.Fatalf("Can't encode: %s", err) 26 | } 27 | 28 | log.Printf("Encoded: %q", encodedValue) 29 | } else { 30 | var decodedValue string 31 | if err := securecookie.DecodeMulti("s", *value, &decodedValue, codecs...); err != nil { 32 | log.Fatalf("Can't decode %q: %s", *value, err) 33 | } 34 | 35 | log.Printf("Decoded: %q", decodedValue) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/reanalyze_repo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | 7 | "github.com/golangci/golangci-api/pkg/worker/analyze/analyzesqueue/repoanalyzesqueue" 8 | 9 | app "github.com/golangci/golangci-api/pkg/api" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/golangci/golangci-api/internal/shared/config" 14 | "github.com/golangci/golangci-api/internal/shared/logutil" 15 | 16 | "github.com/golangci/golangci-api/internal/shared/db/gormdb" 17 | "github.com/golangci/golangci-api/pkg/api/models" 18 | "github.com/jinzhu/gorm" 19 | _ "github.com/joho/godotenv/autoload" 20 | _ "github.com/lib/pq" 21 | ) 22 | 23 | func main() { 24 | repoName := flag.String("repo", "", "owner/name") 25 | flag.Parse() 26 | 27 | if *repoName == "" { 28 | log.Fatalf("Must set --repo") 29 | } 30 | 31 | if err := reanalyzeRepo(*repoName); err != nil { 32 | log.Fatalf("Failed to reanalyze: %s", err) 33 | } 34 | 35 | log.Printf("Successfully reanalyzed repo %s", *repoName) 36 | } 37 | 38 | func reanalyzeRepo(repoName string) error { 39 | a := app.NewApp() 40 | 41 | log := logutil.NewStderrLog("") 42 | cfg := config.NewEnvConfig(log) 43 | db, err := gormdb.GetDB(cfg, log, "") 44 | if err != nil { 45 | return errors.Wrap(err, "failed to get gorm db") 46 | } 47 | 48 | var repo models.Repo 49 | if err = models.NewRepoQuerySet(db).FullNameEq(repoName).One(&repo); err != nil { 50 | return errors.Wrap(err, "failed to get repo by name") 51 | } 52 | 53 | return restartAnalysis(db, &repo, a.GetRepoAnalyzesRunQueue()) 54 | } 55 | 56 | func restartAnalysis(db *gorm.DB, repo *models.Repo, runQueue *repoanalyzesqueue.Producer) error { 57 | var as models.RepoAnalysisStatus 58 | if err := models.NewRepoAnalysisStatusQuerySet(db).RepoIDEq(repo.ID).One(&as); err != nil { 59 | return errors.Wrapf(err, "can't get repo analysis status for repo %d", repo.ID) 60 | } 61 | 62 | var a models.RepoAnalysis 63 | if err := models.NewRepoAnalysisQuerySet(db).RepoAnalysisStatusIDEq(as.ID).OrderDescByID().One(&a); err != nil { 64 | return errors.Wrap(err, "can't get repo analysis") 65 | } 66 | 67 | var auth models.Auth 68 | if err := models.NewAuthQuerySet(db).UserIDEq(repo.UserID).One(&auth); err != nil { 69 | return errors.Wrap(err, "can't get auth") 70 | } 71 | 72 | return runQueue.Put(repo.FullName, a.AnalysisGUID, as.DefaultBranch, auth.PrivateAccessToken, a.CommitSHA) 73 | } 74 | -------------------------------------------------------------------------------- /scripts/recover_pull_analyzes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | app "github.com/golangci/golangci-api/pkg/api" 7 | ) 8 | 9 | func main() { 10 | a := app.NewApp() 11 | if err := a.RecoverAnalyzes(); err != nil { 12 | log.Fatalf("Failed to recover analyzes: %s", err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/activate_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golangci/golangci-api/test/sharedtest" 7 | ) 8 | 9 | func TestActivate(t *testing.T) { 10 | r, u := sharedtest.GetDeactivatedRepo(t) 11 | r.Activate() 12 | u.A.True(u.Repos()[0].IsActivated) 13 | } 14 | 15 | func TestDeactivate(t *testing.T) { 16 | r, u := sharedtest.GetDeactivatedRepo(t) 17 | r.Activate() 18 | r.Deactivate() 19 | u.A.False(u.Repos()[0].IsActivated) 20 | } 21 | 22 | func TestDoubleActivate(t *testing.T) { 23 | r, _ := sharedtest.GetDeactivatedRepo(t) 24 | r.Activate() 25 | r.Activate() 26 | } 27 | 28 | func TestDoubleDeactivate(t *testing.T) { 29 | r, _ := sharedtest.GetDeactivatedRepo(t) 30 | r.Activate() 31 | r.Deactivate() 32 | r.Deactivate() 33 | } 34 | -------------------------------------------------------------------------------- /test/data/github_fake_response/add_hook.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "url": "https://api.github.com/repos/octocat/Hello-World/hooks/1", 4 | "test_url": "https://api.github.com/repos/octocat/Hello-World/hooks/1/test", 5 | "ping_url": "https://api.github.com/repos/octocat/Hello-World/hooks/1/pings", 6 | "name": "web", 7 | "events": [ 8 | "push", 9 | "pull_request" 10 | ], 11 | "active": true, 12 | "config": { 13 | "url": "http://example.com/webhook", 14 | "content_type": "json" 15 | }, 16 | "updated_at": "2011-09-06T20:39:23Z", 17 | "created_at": "2011-09-06T17:26:27Z" 18 | } 19 | -------------------------------------------------------------------------------- /test/data/github_fake_response/get_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "golangci", 3 | "id": 1, 4 | "avatar_url": "https://avatars3.githubusercontent.com/u/1?v=4", 5 | "gravatar_id": "", 6 | "url": "https://api.github.com/users/golangci", 7 | "html_url": "https://github.com/golangci", 8 | "followers_url": "https://api.github.com/users/golangci/followers", 9 | "following_url": "https://api.github.com/users/golangci/following{/other_user}", 10 | "gists_url": "https://api.github.com/users/golangci/gists{/gist_id}", 11 | "starred_url": "https://api.github.com/users/golangci/starred{/owner}{/repo}", 12 | "subscriptions_url": "https://api.github.com/users/golangci/subscriptions", 13 | "organizations_url": "https://api.github.com/users/golangci/orgs", 14 | "repos_url": "https://api.github.com/users/golangci/repos", 15 | "events_url": "https://api.github.com/users/golangci/events{/privacy}", 16 | "received_events_url": "https://api.github.com/users/golangci/received_events", 17 | "type": "User", 18 | "site_admin": false, 19 | "name": "Golang CI", 20 | "blog": "https://github.com/golangci", 21 | "location": "Boston", 22 | "email": "dev@golangci.com", 23 | "hireable": true, 24 | "bio": "Some bio", 25 | "public_repos": 7, 26 | "public_gists": 0, 27 | "followers": 1, 28 | "following": 1, 29 | "created_at": "2017-01-01T01:02:03Z", 30 | "updated_at": "2018-01-01T01:02:03Z" 31 | } 32 | -------------------------------------------------------------------------------- /test/events_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/golangci/golangci-api/test/sharedtest" 8 | ) 9 | 10 | func TestPostAnalytitcsEvent(t *testing.T) { 11 | u := sharedtest.Login(t) 12 | u.E.POST("/v1/events/analytics"). 13 | WithJSON(map[string]interface{}{ 14 | "name": "test", 15 | "payload": map[string]interface{}{ 16 | "a": 1, 17 | "b": "2", 18 | }, 19 | }). 20 | Expect().Status(http.StatusOK) 21 | } 22 | -------------------------------------------------------------------------------- /test/github_login_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/golangci/golangci-api/test/sharedtest" 8 | ) 9 | 10 | func TestGithubLoginFirstTime(t *testing.T) { 11 | u := sharedtest.Login(t) 12 | u.E.PUT("/v1/auth/unlink").Expect().Status(http.StatusOK) 13 | 14 | // it's guaranteed first time login 15 | sharedtest.Login(t) 16 | } 17 | 18 | func TestGithubLoginNotFirstTime(t *testing.T) { 19 | sharedtest.Login(t) 20 | sharedtest.Login(t) 21 | } 22 | 23 | func TestLoginWithAnotherLogin(t *testing.T) { 24 | u := sharedtest.Login(t) 25 | u.A.Equal("golangci", u.GithubLogin) 26 | 27 | defer func(prevProfileHandler http.HandlerFunc) { 28 | sharedtest.FakeGithubCfg.ProfileHandler = prevProfileHandler 29 | }(sharedtest.FakeGithubCfg.ProfileHandler) 30 | 31 | wasSent := false 32 | sharedtest.FakeGithubCfg.ProfileHandler = func(w http.ResponseWriter, r *http.Request) { 33 | ret := map[string]interface{}{ 34 | "login": "AnotherLogin", 35 | "email": "Another_Email@golangci.com", 36 | "id": 1, // the same github user id 37 | "avatar_url": "another Avatar", 38 | "name": "Another Name", 39 | } 40 | 41 | sharedtest.SendJSON(w, ret) 42 | wasSent = true 43 | } 44 | 45 | u2 := sharedtest.Login(t) 46 | u2.A.True(wasSent) 47 | u2.A.Equal("AnotherLogin", u2.GithubLogin) 48 | u2.A.Equal("another_email@golangci.com", u2.Email) 49 | u2.A.Equal("another Avatar", u2.AvatarURL) 50 | u2.A.Equal("Another Name", u2.Name) 51 | u.A.Equal(u.ID, u2.ID) // logined into the same user 52 | } 53 | -------------------------------------------------------------------------------- /test/hooks_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/golangci/golangci-api/pkg/api/crons/pranalyzes" 11 | "github.com/golangci/golangci-api/pkg/api/models" 12 | "github.com/golangci/golangci-api/test/sharedtest" 13 | gh "github.com/google/go-github/github" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestReceivePingWebhook(t *testing.T) { 18 | r, _ := sharedtest.GetActivatedRepo(t) 19 | r.ExpectWebhook("ping", gh.PingEvent{}).Status(http.StatusOK) 20 | } 21 | 22 | func getTestPREvent(r *sharedtest.Repo) gh.PullRequestEvent { 23 | ownerAndName := strings.Split(r.Name, "/") 24 | return gh.PullRequestEvent{ 25 | Action: gh.String("opened"), 26 | PullRequest: &gh.PullRequest{ 27 | Number: gh.Int(1), 28 | Head: &gh.PullRequestBranch{ 29 | SHA: gh.String(fmt.Sprintf("sha_%d", time.Now().UnixNano())), 30 | }, 31 | }, 32 | Repo: &gh.Repository{ 33 | Owner: &gh.User{ 34 | Login: gh.String(ownerAndName[0]), 35 | }, 36 | Name: gh.String(ownerAndName[1]), 37 | }, 38 | } 39 | } 40 | 41 | func TestReceivePullRequestOpenedWebhook(t *testing.T) { 42 | r, _ := sharedtest.GetActivatedRepo(t) 43 | r.ExpectWebhook("pull_request", getTestPREvent(r)).Status(http.StatusOK) 44 | } 45 | 46 | func TestReceivePushWebhook(t *testing.T) { 47 | r, _ := sharedtest.GetActivatedRepo(t) 48 | r.ExpectWebhook("push", gh.PushEvent{ 49 | Ref: gh.String("refs/heads/master"), 50 | Repo: &gh.PushEventRepository{ 51 | DefaultBranch: gh.String("master"), 52 | }, 53 | HeadCommit: &gh.PushEventCommit{ 54 | ID: gh.String("sha"), 55 | }, 56 | }).Status(http.StatusOK) 57 | } 58 | 59 | func TestStaleAnalyzes(t *testing.T) { 60 | r, _ := sharedtest.GetActivatedRepo(t) 61 | deps := sharedtest.GetDefaultTestApp().BuildCommonDeps() 62 | 63 | sharedtest.GetDefaultTestApp().PurgeAnalyzesQueue(t) 64 | err := models.NewPullRequestAnalysisQuerySet(deps.DB).Delete() 65 | assert.NoError(t, err) 66 | 67 | r.ExpectWebhook("pull_request", getTestPREvent(r)).Status(http.StatusOK) 68 | 69 | timeout := 10 * time.Second 70 | staler := pranalyzes.Staler{ 71 | Cfg: deps.Cfg, 72 | DB: deps.DB, 73 | Log: deps.Log, 74 | ProviderFactory: deps.ProviderFactory, 75 | } 76 | staleCount, err := staler.RunIteration(timeout) 77 | assert.NoError(t, err) 78 | assert.Zero(t, staleCount) 79 | 80 | time.Sleep(timeout + time.Millisecond) 81 | 82 | staleCount, err = staler.RunIteration(timeout) 83 | assert.NoError(t, err) 84 | assert.Equal(t, 1, staleCount) 85 | } 86 | -------------------------------------------------------------------------------- /test/list_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golangci/golangci-api/test/sharedtest" 7 | ) 8 | 9 | func TestListRepos(t *testing.T) { 10 | u := sharedtest.Login(t) 11 | u.Repos() 12 | } 13 | 14 | func TestGithubPrivateLogin(t *testing.T) { 15 | u := sharedtest.Login(t) 16 | u.A.True(u.GithubPrivateLogin().WerePrivateReposFetched()) 17 | } 18 | -------------------------------------------------------------------------------- /test/prepare_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | echo 'REDIS_URL="redis://localhost:6379"' >>.env 3 | echo 'WEB_ROOT="https://golangci.com"' >>.env 4 | echo MIGRATIONS_PATH=../migrations >>.env 5 | echo PATCH_STORE_DIR=/go >>.env 6 | echo 'DATABASE_URL="postgresql://postgres:test@localhost:5432/api_test?sslmode=disable"' >>.env.test 7 | echo 'SESSION_SECRET="123123123"' >> .env.test 8 | echo GITHUB_KEY=GK >>.env.test 9 | echo GITHUB_SECRET=GS >>.env.test 10 | echo GITHUB_CALLBACK_HOST=GCH >>.env.test 11 | echo SQS_PRIMARYDEADLETTER_QUEUE_URL=123 >>.env.test 12 | -------------------------------------------------------------------------------- /test/sharedtest/app.go: -------------------------------------------------------------------------------- 1 | package sharedtest 2 | 3 | import ( 4 | "log" 5 | "net/http/httptest" 6 | "os" 7 | "path" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/golangci/golangci-api/internal/shared/fsutil" 13 | app "github.com/golangci/golangci-api/pkg/api" 14 | "github.com/joho/godotenv" 15 | ) 16 | 17 | type App struct { 18 | app *app.App 19 | testserver *httptest.Server 20 | fakeGithubServer *httptest.Server 21 | } 22 | 23 | func (a App) PurgeAnalyzesQueue(t *testing.T) { 24 | assert.NoError(t, a.app.PurgeAnalyzesQueue()) 25 | } 26 | 27 | func RunApp() *App { 28 | loadEnv() 29 | 30 | ta := App{} 31 | ta.initFakeGithubServer() 32 | 33 | deps := ta.BuildCommonDeps() 34 | 35 | modifiers := []app.Modifier{ 36 | app.SetProviderFactory(deps.ProviderFactory), 37 | } 38 | 39 | ta.app = app.NewApp(modifiers...) 40 | 41 | ta.testserver = httptest.NewServer(ta.app.GetHTTPHandler()) 42 | os.Setenv("GITHUB_CALLBACK_HOST", ta.testserver.URL) 43 | os.Setenv("WEB_ROOT", ta.testserver.URL) 44 | 45 | ta.app.RunEnvironment() 46 | 47 | return &ta 48 | } 49 | 50 | func loadEnv() { 51 | envNames := []string{".env", ".env.test"} 52 | for _, envName := range envNames { 53 | fpath := path.Join(fsutil.GetProjectRoot(), envName) 54 | err := godotenv.Overload(fpath) 55 | if err != nil { 56 | log.Fatalf("Can't load %s: %s", fpath, err) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/sharedtest/auth.go: -------------------------------------------------------------------------------- 1 | package sharedtest 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/gavv/httpexpect" 11 | "github.com/golangci/golangci-api/pkg/api/returntypes" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type User struct { 16 | returntypes.AuthorizedUser 17 | A *require.Assertions 18 | E *httpexpect.Expect 19 | t *testing.T 20 | 21 | testApp *App 22 | } 23 | 24 | func (ta App) newHTTPExpect(t *testing.T) *httpexpect.Expect { 25 | httpClient := &http.Client{ 26 | Jar: httpexpect.NewJar(), 27 | CheckRedirect: func(req *http.Request, _ []*http.Request) error { 28 | isRedirectToFakeGithub := strings.HasPrefix(req.URL.String(), ta.fakeGithubServer.URL) 29 | if isRedirectToFakeGithub || strings.HasPrefix(req.URL.Path, "/v1/auth/github") { 30 | return nil // follow redirect 31 | } 32 | 33 | return http.ErrUseLastResponse // don't follow redirect: it's redirect after successful login 34 | }, 35 | } 36 | 37 | return httpexpect.WithConfig(httpexpect.Config{ 38 | BaseURL: ta.testserver.URL, 39 | Reporter: httpexpect.NewAssertReporter(t), 40 | Printers: []httpexpect.Printer{ 41 | httpexpect.NewCompactPrinter(t), 42 | }, 43 | Client: httpClient, 44 | }) 45 | } 46 | 47 | func (ta App) Login(t *testing.T) *User { 48 | e := ta.newHTTPExpect(t) 49 | 50 | e.GET("/v1/auth/check"). 51 | Expect(). 52 | Status(http.StatusForbidden) 53 | 54 | e.GET("/v1/auth/github"). 55 | Expect(). 56 | Status(http.StatusTemporaryRedirect). 57 | Header("Location"). 58 | Equal(os.Getenv("WEB_ROOT") + "/repos/github?after=login") 59 | 60 | checkBody := e.GET("/v1/auth/check"). 61 | Expect(). 62 | Status(http.StatusOK). 63 | Body(). 64 | Raw() 65 | 66 | userResp := make(map[string]*User) 67 | require.NoError(t, json.Unmarshal([]byte(checkBody), &userResp)) 68 | user := userResp["user"] 69 | require.NotNil(t, user) 70 | require.NotZero(t, user.ID) 71 | 72 | user.A = require.New(t) 73 | user.E = e 74 | user.t = t 75 | user.testApp = &ta 76 | return user 77 | } 78 | 79 | func (u *User) GithubPrivateLogin() *User { 80 | u.E.GET("/v1/auth/github/private"). 81 | Expect(). 82 | Status(http.StatusTemporaryRedirect). 83 | Header("Location"). 84 | Equal(os.Getenv("WEB_ROOT") + "/repos/github?refresh=1&after=private_login") 85 | return u 86 | } 87 | -------------------------------------------------------------------------------- /test/sharedtest/common_deps.go: -------------------------------------------------------------------------------- 1 | package sharedtest 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/config" 5 | "github.com/golangci/golangci-api/internal/shared/db/gormdb" 6 | "github.com/golangci/golangci-api/internal/shared/logutil" 7 | "github.com/golangci/golangci-api/internal/shared/providers" 8 | "github.com/golangci/golangci-api/internal/shared/providers/provider" 9 | "github.com/golangci/golangci-api/test/sharedtest/mocks" 10 | "github.com/jinzhu/gorm" 11 | ) 12 | 13 | type CommonDeps struct { 14 | DB *gorm.DB 15 | Cfg config.Config 16 | Log logutil.Log 17 | ProviderFactory providers.Factory 18 | } 19 | 20 | func (ta *App) BuildCommonDeps() *CommonDeps { 21 | log := logutil.NewStderrLog("test") 22 | cfg := config.NewEnvConfig(log) 23 | 24 | dbConnString, err := gormdb.GetDBConnString(cfg) 25 | if err != nil { 26 | log.Fatalf("Can't get DB conn string: %s", err) 27 | } 28 | 29 | gormDB, err := gormdb.GetDB(cfg, log, dbConnString) 30 | if err != nil { 31 | log.Fatalf("Can't get gorm db: %s", err) 32 | } 33 | 34 | origPF := providers.NewBasicFactory(log) 35 | pf := mocks.NewProviderFactory(func(p provider.Provider) provider.Provider { 36 | if err := p.SetBaseURL(ta.fakeGithubServer.URL + "/"); err != nil { 37 | log.Fatalf("Failed to set base url: %s", err) 38 | } 39 | return p 40 | }, origPF) 41 | 42 | return &CommonDeps{ 43 | DB: gormDB, 44 | Cfg: cfg, 45 | Log: log, 46 | ProviderFactory: pf, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/sharedtest/default_app.go: -------------------------------------------------------------------------------- 1 | package sharedtest 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | var makeDefaultAppOnce sync.Once 9 | var defaultTestApp *App 10 | 11 | func GetDefaultTestApp() *App { 12 | makeDefaultAppOnce.Do(func() { 13 | defaultTestApp = RunApp() 14 | }) 15 | return defaultTestApp 16 | } 17 | 18 | func Login(t *testing.T) *User { 19 | return GetDefaultTestApp().Login(t) 20 | } 21 | -------------------------------------------------------------------------------- /test/sharedtest/mocks/provider_factory.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/golangci/golangci-api/internal/shared/providers" 5 | "github.com/golangci/golangci-api/internal/shared/providers/provider" 6 | "github.com/golangci/golangci-api/pkg/api/models" 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | type ProviderTransformer func(p provider.Provider) provider.Provider 11 | 12 | type ProviderFactory struct { 13 | orig providers.Factory 14 | transformer ProviderTransformer 15 | } 16 | 17 | var _ providers.Factory = &ProviderFactory{} 18 | 19 | func NewProviderFactory(transformer ProviderTransformer, orig providers.Factory) *ProviderFactory { 20 | return &ProviderFactory{ 21 | orig: orig, 22 | transformer: transformer, 23 | } 24 | } 25 | 26 | func (f ProviderFactory) Build(auth *models.Auth) (provider.Provider, error) { 27 | p, err := f.orig.Build(auth) 28 | if p != nil { 29 | p = f.transformer(p) 30 | } 31 | return p, err 32 | } 33 | 34 | func (f ProviderFactory) BuildForUser(db *gorm.DB, userID uint) (provider.Provider, error) { 35 | p, err := f.orig.BuildForUser(db, userID) 36 | if p != nil { 37 | p = f.transformer(p) 38 | } 39 | return p, err 40 | } 41 | 42 | func (f ProviderFactory) BuildForToken(providerName, accessToken string) (provider.Provider, error) { 43 | p, err := f.orig.BuildForToken(providerName, accessToken) 44 | if p != nil { 45 | p = f.transformer(p) 46 | } 47 | return p, err 48 | } 49 | --------------------------------------------------------------------------------