├── .env.example ├── .gitignore ├── .gitlab-ci.yml ├── .php_cs ├── .phpstorm.meta.php ├── Dockerfile ├── README.md ├── app ├── Controller │ ├── AbstractController.php │ ├── DebugController.php │ ├── IndexController.php │ └── WebhookController.php ├── Event │ └── ReceivedPullRequest.php ├── EventHandler │ ├── AbstractHandler.php │ ├── CommandManager.php │ ├── EventHandlerManager.php │ ├── IssueCommentHandler.php │ ├── PullRequestHandler.php │ └── PullRequestReviewHandler.php ├── Exception │ ├── EventHandlerNotExistException.php │ ├── Handler │ │ └── AppExceptionHandler.php │ └── RuntimeException.php ├── Service │ ├── EndpointInterface.php │ ├── Endpoints │ │ ├── AbstractEnpoint.php │ │ ├── ApprovePullRequest.php │ │ ├── Assign.php │ │ ├── Log.php │ │ ├── MergePullRequest.php │ │ ├── NeedReview.php │ │ ├── RemoveAssign.php │ │ └── RequestChanges.php │ ├── GithubService.php │ └── SignatureService.php ├── Traits │ ├── ClientTrait.php │ └── CommentTrait.php └── Utils │ ├── GithubClientBuilder.php │ └── GithubUrlBuilder.php ├── bin └── hyperf.php ├── composer.json ├── config ├── autoload │ ├── annotations.php │ ├── aspects.php │ ├── dependencies.php │ ├── exceptions.php │ ├── github.php │ ├── logger.php │ └── server.php ├── config.php ├── container.php └── routes.php ├── deploy.test.yml ├── phpstan.neon ├── phpunit.xml └── test ├── Cases └── ExampleTest.php ├── HttpTestCase.php └── bootstrap.php /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=skeleton 2 | 3 | DB_DRIVER=mysql 4 | DB_HOST=localhost 5 | DB_PORT=3306 6 | DB_DATABASE=hyperf 7 | DB_USERNAME=root 8 | DB_PASSWORD= 9 | DB_CHARSET=utf8mb4 10 | DB_COLLATION=utf8mb4_unicode_ci 11 | DB_PREFIX= 12 | 13 | REDIS_HOST=localhost 14 | REDIS_AUTH=(null) 15 | REDIS_PORT=6379 16 | REDIS_DB=0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | *.lock 13 | .phpunit* -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # usermod -aG docker gitlab-runner 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | variables: 8 | PROJECT_NAME: hyperf 9 | REGISTRY_URL: registry-docker.org 10 | 11 | build_test_docker: 12 | stage: build 13 | before_script: 14 | # - git submodule sync --recursive 15 | # - git submodule update --init --recursive 16 | script: 17 | - docker build . -t $PROJECT_NAME 18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test 19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test 20 | only: 21 | - test 22 | tags: 23 | - builder 24 | 25 | deploy_test_docker: 26 | stage: deploy 27 | script: 28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME 29 | only: 30 | - test 31 | tags: 32 | - test 33 | 34 | build_docker: 35 | stage: build 36 | before_script: 37 | # - git submodule sync --recursive 38 | # - git submodule update --init --recursive 39 | script: 40 | - docker build . -t $PROJECT_NAME 41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest 43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest 45 | only: 46 | - tags 47 | tags: 48 | - builder 49 | 50 | deploy_docker: 51 | stage: deploy 52 | script: 53 | - echo SUCCESS 54 | only: 55 | - tags 56 | tags: 57 | - builder 58 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 10 | ->setRules([ 11 | '@PSR2' => true, 12 | '@Symfony' => true, 13 | '@DoctrineAnnotation' => true, 14 | '@PhpCsFixer' => true, 15 | 'header_comment' => [ 16 | 'commentType' => 'PHPDoc', 17 | 'header' => $header, 18 | 'separate' => 'none', 19 | 'location' => 'after_declare_strict', 20 | ], 21 | 'array_syntax' => [ 22 | 'syntax' => 'short' 23 | ], 24 | 'list_syntax' => [ 25 | 'syntax' => 'short' 26 | ], 27 | 'concat_space' => [ 28 | 'spacing' => 'one' 29 | ], 30 | 'blank_line_before_statement' => [ 31 | 'statements' => [ 32 | 'declare', 33 | ], 34 | ], 35 | 'general_phpdoc_annotation_remove' => [ 36 | 'annotations' => [ 37 | 'author' 38 | ], 39 | ], 40 | 'ordered_imports' => [ 41 | 'imports_order' => [ 42 | 'class', 'function', 'const', 43 | ], 44 | 'sort_algorithm' => 'alpha', 45 | ], 46 | 'single_line_comment_style' => [ 47 | 'comment_types' => [ 48 | ], 49 | ], 50 | 'yoda_style' => [ 51 | 'always_move_variable' => false, 52 | 'equal' => false, 53 | 'identical' => false, 54 | ], 55 | 'phpdoc_align' => [ 56 | 'align' => 'left', 57 | ], 58 | 'multiline_whitespace_before_semicolons' => [ 59 | 'strategy' => 'no_multi_line', 60 | ], 61 | 'constant_case' => [ 62 | 'case' => 'lower', 63 | ], 64 | 'class_attributes_separation' => true, 65 | 'combine_consecutive_unsets' => true, 66 | 'declare_strict_types' => true, 67 | 'linebreak_after_opening_tag' => true, 68 | 'lowercase_static_reference' => true, 69 | 'no_useless_else' => true, 70 | 'no_unused_imports' => true, 71 | 'not_operator_with_successor_space' => true, 72 | 'not_operator_with_space' => false, 73 | 'ordered_class_elements' => true, 74 | 'php_unit_strict' => false, 75 | 'phpdoc_separation' => false, 76 | 'single_quote' => true, 77 | 'standardize_not_equals' => true, 78 | 'multiline_comment_opening_closing' => true, 79 | ]) 80 | ->setFinder( 81 | PhpCsFixer\Finder::create() 82 | ->exclude('public') 83 | ->exclude('runtime') 84 | ->exclude('vendor') 85 | ->in(__DIR__) 86 | ) 87 | ->setUsingCache(false); 88 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | " version="1.0" license="MIT" app.name="Hyperf" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | COMPOSER_VERSION=1.9.1 \ 19 | APP_ENV=prod \ 20 | SCAN_CACHEABLE=(true) 21 | 22 | # update 23 | RUN set -ex \ 24 | && apk update \ 25 | # install composer 26 | && cd /tmp \ 27 | && wget https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ 28 | && chmod u+x composer.phar \ 29 | && mv composer.phar /usr/local/bin/composer \ 30 | # show php version and extensions 31 | && php -v \ 32 | && php -m \ 33 | && php --ri swoole \ 34 | # ---------- some config ---------- 35 | && cd /etc/php7 \ 36 | # - config PHP 37 | && { \ 38 | echo "upload_max_filesize=100M"; \ 39 | echo "post_max_size=108M"; \ 40 | echo "memory_limit=1024M"; \ 41 | echo "date.timezone=${TIMEZONE}"; \ 42 | } | tee conf.d/99_overrides.ini \ 43 | # - config timezone 44 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 45 | && echo "${TIMEZONE}" > /etc/timezone \ 46 | # ---------- clear works ---------- 47 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 48 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 49 | 50 | WORKDIR /opt/www 51 | 52 | # Composer Cache 53 | # COPY ./composer.* /opt/www/ 54 | # RUN composer install --no-dev --no-scripts 55 | 56 | COPY . /opt/www 57 | RUN composer install --no-dev -o && php bin/hyperf.php 58 | 59 | EXPOSE 9501 60 | 61 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Bot 2 | A robot for manage github repository via command of issue comment or pull request comment. 3 | The robot based on [Hyperf framework](https://github.com/hyperf/hyperf). 4 | 5 | ## Commands 6 | All commands should place in a new line, and then the Github-Bot will follow the lines to execute the command. 7 | 8 | ### `/merge` 9 | The bot will merge the Pull Request automatically. 10 | 11 | ### `/assign [user]` 12 | Assign the issue or pull request to the users. 13 | Parameters: 14 | `user`: *[REQUIRED]* the users who you want to assign. 15 | Example: 16 | One user: `/assign @huangzhhui` 17 | Many user: `/assign @huangzhhui @huangzhhui` 18 | 19 | ### `/remove-assign [user]` 20 | Remove the users from assignees. 21 | Parameters: 22 | `user`: *[REQUIRED]* the users who you want to remove. 23 | Example: 24 | One user: `/remove-assign @huangzhhui` 25 | Many user: `/remove-assign @huangzhhui @huangzhhui` 26 | 27 | ### `/need-review [user]` 28 | Assign the users as reviewers. 29 | Parameters: 30 | `user`: *[REQUIRED]* the users who you want to assign as reviewers. 31 | Example: 32 | One user: `/need-review @huangzhhui` 33 | Many user: `/need-review @huangzhhui @huangzhhui` 34 | -------------------------------------------------------------------------------- /app/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | request->hasHeader('Authorization') || $this->request->getHeaderLine('Authorization') !== $this->getDebugAuth()) { 39 | return $this->response->withStatus('401'); 40 | } 41 | $event = $this->request->input('event'); 42 | $handler = $this->eventHandlerManager->getHandler($event); 43 | return $handler->handle($this->request); 44 | } 45 | 46 | public function getDebugAuth(): string 47 | { 48 | return $this->debugAuth; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | debugAuth) { 48 | if (! $this->isValidWebhook($this->request)) { 49 | return $this->response->withStatus(400); 50 | } 51 | if (! $this->signatureService->isValid($this->request)) { 52 | return $this->response->withStatus(401); 53 | } 54 | } 55 | $event = $this->request->getHeaderLine(GithubService::HEADER_EVENT); 56 | $handler = $this->eventHandlerManager->getHandler($event); 57 | return $handler->handle($this->request); 58 | } 59 | 60 | private function isValidWebhook(RequestInterface $request): bool 61 | { 62 | return $request->hasHeader(GithubService::HEADER_EVENT) && $request->hasHeader(GithubService::HEADER_SIGNATURE) && $request->getHeaderLine('content-type') === 'application/json'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Event/ReceivedPullRequest.php: -------------------------------------------------------------------------------- 1 | request = $request; 28 | $this->response = $response; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/EventHandler/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | get(ResponseInterface::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/EventHandler/CommandManager.php: -------------------------------------------------------------------------------- 1 | parseIssueId($target); 52 | switch ($prefix) { 53 | case '/merge': 54 | $endPoint = new Endpoints\MergePullRequest($repository, $pullRequestId, $target); 55 | break; 56 | case '/request-changes': 57 | case '/request_changes': 58 | case '/requestchanges': 59 | $body = $this->parseBody($explodedCommand); 60 | $endPoint = new Endpoints\RequestChanges($repository, $pullRequestId, $body); 61 | break; 62 | case '/approve': 63 | $endPoint = new Endpoints\ApprovePullRequest($repository, $pullRequestId, ''); 64 | break; 65 | case '/assign': 66 | $body = $this->parseBody($explodedCommand); 67 | $endPoint = new Endpoints\Assign($repository, $pullRequestId, $body); 68 | break; 69 | case '/remove-assign': 70 | case '/remove_assign': 71 | case '/removeassign': 72 | $body = $this->parseBody($explodedCommand); 73 | $endPoint = new Endpoints\RemoveAssign($repository, $pullRequestId, $body); 74 | break; 75 | case '/need-review': 76 | case '/need_review': 77 | case '/needreview': 78 | $body = $this->parseBody($explodedCommand); 79 | $endPoint = new Endpoints\NeedReview($repository, $pullRequestId, $body); 80 | break; 81 | case '/log': 82 | $endPoint = new Endpoints\Log($target); 83 | break; 84 | } 85 | if ($endPoint instanceof EndpointInterface) { 86 | $this->logger->debug(sprintf('Trigger command: %s', $prefix)); 87 | $this->githubService->execute($endPoint); 88 | } 89 | } 90 | 91 | public function isValidCommand(string $command): bool 92 | { 93 | if (! Str::startsWith($command, $this->commands)) { 94 | return false; 95 | } 96 | return true; 97 | } 98 | 99 | /** 100 | * @return mixed 101 | */ 102 | private function parseIssueId(array $target) 103 | { 104 | if (isset($target['issue'])) { 105 | $pullRequestId = $target['issue']['number']; 106 | } else { 107 | $pullRequestId = $target['pull_request']['number']; 108 | } 109 | return $pullRequestId; 110 | } 111 | 112 | private function parseBody(array $explodedCommand): string 113 | { 114 | unset($explodedCommand[0]); 115 | return implode(' ', $explodedCommand); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/EventHandler/EventHandlerManager.php: -------------------------------------------------------------------------------- 1 | IssueCommentHandler::class, 21 | 'pull_request_review' => PullRequestReviewHandler::class, 22 | 'pull_request' => PullRequestHandler::class 23 | ]; 24 | 25 | /** 26 | * @Inject() 27 | * @var LoggerInterface 28 | */ 29 | protected $logger; 30 | 31 | /** 32 | * Get the specified event handler. 33 | * 34 | * @throws EventHandlerNotExistException 35 | */ 36 | public function getHandler(string $event): AbstractHandler 37 | { 38 | $this->logger->debug(sprintf('Receive %s event.', $event)); 39 | if (! isset($this->events[$event])) { 40 | throw new EventHandlerNotExistException('Event handler not exist.'); 41 | } 42 | $handler = new ($this->events[$event]); 43 | if (! $handler instanceof AbstractHandler) { 44 | throw new EventHandlerNotExistException('It is not a valid event handler.'); 45 | } 46 | return $handler; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/EventHandler/IssueCommentHandler.php: -------------------------------------------------------------------------------- 1 | logger->debug('Receive a issue comment request.'); 35 | $issue = $request->all(); 36 | $comment = $request->input('comment.body', []); 37 | if (! $issue || ! $comment) { 38 | $message = 'Invalid argument.'; 39 | $this->logger->debug($message); 40 | return $this->response()->withStatus(400, $message); 41 | } 42 | if (! $this->isValidUser($issue)) { 43 | $message = 'Invalid user operation.'; 44 | $this->logger->debug($message); 45 | return $this->response()->withStatus(401, $message); 46 | } 47 | $commands = $this->parseCommands($comment); 48 | if (! $commands) { 49 | $this->logger->debug('Receive a request, but no command.'); 50 | } 51 | foreach ($commands as $command) { 52 | $this->commandManager->execute($command, $issue); 53 | } 54 | return $this->response()->withStatus(200); 55 | } 56 | 57 | protected function parseCommands(string $body): array 58 | { 59 | $commands = []; 60 | $delimiter = "\r\n"; 61 | $comments = explode($delimiter, $body); 62 | foreach ($comments as $comment) { 63 | if ($this->commandManager->isValidCommand($comment)) { 64 | $commands[] = $comment; 65 | } 66 | } 67 | return $commands; 68 | } 69 | 70 | protected function isValidUser(array $issue): bool 71 | { 72 | return isset($issue['comment']['author_association']) && in_array($issue['comment']['author_association'], [ 73 | 'MEMBER', 74 | 'OWNER', 75 | ], true); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/EventHandler/PullRequestHandler.php: -------------------------------------------------------------------------------- 1 | logger->debug('Receive a new pull requests.'); 35 | $response = $this->response()->withStatus(200); 36 | $event = new ReceivedPullRequest($request, $response); 37 | $this->eventDispatcher->dispatch($event); 38 | return $event->response; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/EventHandler/PullRequestReviewHandler.php: -------------------------------------------------------------------------------- 1 | logger->debug('Receive a pull request review request.'); 34 | $issue = $request->input(null, []); 35 | $comment = $request->input('review.body', []); 36 | if (! $issue || ! $comment) { 37 | $message = 'Invalid argument.'; 38 | $this->logger->debug($message); 39 | return $this->response()->withStatus(400, $message); 40 | } 41 | $commands = $this->parseCommands($comment); 42 | if (! $commands) { 43 | $this->logger->debug('Receive a request, but no command.'); 44 | } 45 | foreach ($commands as $command) { 46 | $this->commandManager->execute($command, $issue); 47 | } 48 | return $this->response()->withStatus(200); 49 | } 50 | 51 | protected function parseCommands(string $body): array 52 | { 53 | $commands = []; 54 | $delimiter = "\r\n"; 55 | $comments = explode($delimiter, $body); 56 | foreach ($comments as $comment) { 57 | if ($this->commandManager->isValidCommand($comment)) { 58 | $commands[] = $comment; 59 | } 60 | } 61 | return $commands; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Exception/EventHandlerNotExistException.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 26 | } 27 | 28 | public function handle(Throwable $throwable, ResponseInterface $response) 29 | { 30 | $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); 31 | $this->logger->error($throwable->getTraceAsString()); 32 | return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); 33 | } 34 | 35 | public function isValid(Throwable $throwable): bool 36 | { 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | body); 28 | foreach ($explodedBody as $user) { 29 | if (Str::startsWith($user, '@')) { 30 | $reviewers[] = substr($user, 1); 31 | } 32 | } 33 | return array_unique($reviewers); 34 | } 35 | 36 | protected function addApprovedComment($repository, $pullRequestId): void 37 | { 38 | try { 39 | $uri = GithubUrlBuilder::buildReviewsUrl($repository, $pullRequestId); 40 | $response = $this->getClient()->get($uri); 41 | if ($response->getStatusCode() === 200 && $content = $response->getBody()->getContents()) { 42 | $approvedUsers = []; 43 | $decodedBody = json_decode($content, true); 44 | foreach ($decodedBody ?? [] as $review) { 45 | if (isset($review['user']['login'], $review['state']) && $review['state'] === 'APPROVED') { 46 | $approvedUsers[$review['user']['login']] = '[' . $review['user']['login'] . '](' . $review['html_url'] . ')'; 47 | } 48 | } 49 | if ($approvedUsers) { 50 | $comment = "[APPROVAL NOTIFIER] This pull-request is **APPROVED**\r\n\r\nThis pull-request has been approved by: " . implode(' ', $approvedUsers); 51 | $this->addComment($comment); 52 | } 53 | } 54 | } catch (\Throwable $e) { 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Service/Endpoints/ApprovePullRequest.php: -------------------------------------------------------------------------------- 1 | addApprovedComment($this->repository, $this->pullRequestId); 15 | $this->review('APPROVE'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Service/Endpoints/Assign.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 33 | $this->issueId = $issueId; 34 | $this->body = $body; 35 | } 36 | 37 | public function __invoke() 38 | { 39 | $client = $this->getClient(); 40 | $assigneesUrl = GithubUrlBuilder::buildAssigneesUrl($this->repository, $this->issueId); 41 | $assignUsers = $this->parseTargetUsers(); 42 | if (! $assignUsers) { 43 | return; 44 | } 45 | $response = $client->post($assigneesUrl, [ 46 | 'json' => [ 47 | 'assignees' => $assignUsers, 48 | ], 49 | ]); 50 | if ($response->getStatusCode() !== 201) { 51 | Coroutine::sleep(10); 52 | $this->addSorryComment(); 53 | } 54 | } 55 | 56 | private function addSorryComment(): void 57 | { 58 | $this->addComment('( Ĭ ^ Ĭ ) Assign failed, sorry ~~~'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Service/Endpoints/Log.php: -------------------------------------------------------------------------------- 1 | target = $target; 31 | } 32 | 33 | public function __invoke() 34 | { 35 | $this->logger->info(json_encode($this->target)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Service/Endpoints/MergePullRequest.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 33 | $this->pullRequestId = $pullRequestId; 34 | $this->target = $target; 35 | } 36 | 37 | public function __invoke() 38 | { 39 | $this->addApprovedComment($this->repository, $this->pullRequestId); 40 | $mergeUrl = GithubUrlBuilder::buildPullRequestUrl($this->repository, $this->pullRequestId) . '/merge'; 41 | $params = [ 42 | 'merge_method' => config('github.merge.method', 'squash'), 43 | ]; 44 | $pullRequestTitle = value(function () { 45 | if (isset($this->target['issue']['title'])) { 46 | return str_replace([ 47 | 'title', 'url', 48 | ], [ 49 | $this->target['issue']['title'], 50 | $this->target['issue']['pull_request']['html_url'], 51 | ], 'title (url)'); 52 | } 53 | return ''; 54 | }); 55 | $pullRequestTitle && $params['commit_title'] = $pullRequestTitle; 56 | $response = $this->getClient()->put($mergeUrl, [ 57 | 'json' => $params, 58 | ]); 59 | if ($response->getStatusCode() !== 200) { 60 | // Add a comment to notice the member the merge operation failure. 61 | Coroutine::sleep(10); 62 | $this->addComment('( Ĭ ^ Ĭ ) Merge the pull request failed, please help me ~~~'); 63 | echo $response->getStatusCode() . ':' . $response->getBody()->getContents() . PHP_EOL; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Service/Endpoints/NeedReview.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 33 | $this->pullRequestId = $pullRequestId; 34 | $this->body = $body; 35 | } 36 | 37 | public function __invoke() 38 | { 39 | $client = $this->getClient(); 40 | $assigneesUrl = GithubUrlBuilder::buildReviewRequestUrl($this->repository, $this->pullRequestId); 41 | $reviewers = $this->parseTargetUsers(); 42 | if (! $reviewers) { 43 | return; 44 | } 45 | $response = $client->post($assigneesUrl, [ 46 | 'json' => [ 47 | 'reviewers' => $reviewers, 48 | ], 49 | ]); 50 | if ($response->getStatusCode() !== 201) { 51 | Coroutine::sleep(10); 52 | $this->addSorryComment(); 53 | } 54 | } 55 | 56 | private function addSorryComment(): void 57 | { 58 | $this->addComment('( Ĭ ^ Ĭ ) Assign the reviewers failed, sorry ~~~'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Service/Endpoints/RemoveAssign.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 33 | $this->issueId = $issueId; 34 | $this->body = $body; 35 | } 36 | 37 | public function __invoke() 38 | { 39 | $client = $this->getClient(); 40 | $assigneesUrl = GithubUrlBuilder::buildAssigneesUrl($this->repository, $this->issueId); 41 | $assignees = $this->parseTargetUsers(); 42 | if (! $assignees) { 43 | return; 44 | } 45 | $response = $client->delete($assigneesUrl, [ 46 | 'json' => [ 47 | 'assignees' => $assignees, 48 | ], 49 | ]); 50 | if ($response->getStatusCode() !== 201) { 51 | Coroutine::sleep(10); 52 | $this->addSorryComment(); 53 | } 54 | } 55 | 56 | private function addSorryComment(): void 57 | { 58 | $this->addComment('( Ĭ ^ Ĭ ) Remove the assignees failed, sorry ~~~'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Service/Endpoints/RequestChanges.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 33 | $this->pullRequestId = $pullRequestId; 34 | $this->body = $body; 35 | } 36 | 37 | public function __invoke() 38 | { 39 | return $this->review('REQUEST_CHANGES'); 40 | } 41 | 42 | protected function review($status): ResponseInterface 43 | { 44 | $pullRequestUrl = GithubUrlBuilder::buildPullRequestUrl($this->repository, $this->pullRequestId) . '/reviews'; 45 | return $this->getClient()->post($pullRequestUrl, [ 46 | 'json' => [ 47 | 'body' => $this->body, 48 | 'event' => $status, 49 | ], 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Service/GithubService.php: -------------------------------------------------------------------------------- 1 | getHeaderLine(GithubService::HEADER_SIGNATURE), 2); 27 | $payloadHash = hash_hmac($algo, $request->getBody()->getContents() ?? '', $this->getSecret()); 28 | return $payloadHash === $hash; 29 | } 30 | 31 | public function getSecret(): string 32 | { 33 | return $this->secret; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Traits/ClientTrait.php: -------------------------------------------------------------------------------- 1 | get(ClientFactory::class); 19 | return $clientFactory->create([ 20 | 'base_uri' => $baseUri, 21 | 'headers' => [ 22 | 'User-Agent' => config('github.user_agent', 'Github-Bot'), 23 | 'Authorization' => 'token ' . config('github.access_token'), 24 | ], 25 | '_options' => [ 26 | 'timeout' => 60, 27 | ], 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Traits/CommentTrait.php: -------------------------------------------------------------------------------- 1 | repository; 18 | $pullRequestId = $pullRequestId ?? $this->pullRequestId; 19 | $comment = config('github.comment.header') . $comment; 20 | $uri = GithubUrlBuilder::buildIssueUrl($repository, $pullRequestId) . '/comments'; 21 | return $this->getClient()->post($uri, [ 22 | 'json' => [ 23 | 'body' => $comment, 24 | ], 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Utils/GithubClientBuilder.php: -------------------------------------------------------------------------------- 1 | $baseUri, 21 | 'headers' => [ 22 | 'User-Agent' => config('github.user_agent', 'Github-Bot'), 23 | 'Authorization' => 'token ' . config('github.access_token'), 24 | ], 25 | '_options' => [ 26 | 'timeout' => 60, 27 | ], 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Utils/GithubUrlBuilder.php: -------------------------------------------------------------------------------- 1 | get(\Hyperf\Contract\ApplicationInterface::class); 21 | $application->run(); 22 | })(); 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/hyperf-skeleton", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware" 11 | ], 12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 13 | "license": "Apache-2.0", 14 | "require": { 15 | "php": ">=7.2", 16 | "ext-swoole": ">=4.5", 17 | "hyperf/command": "~2.0.0", 18 | "hyperf/config": "~2.0.0", 19 | "hyperf/framework": "~2.0.0", 20 | "hyperf/guzzle": "~2.0.0", 21 | "hyperf/http-server": "~2.0.0", 22 | "hyperf/logger": "~2.0.0" 23 | }, 24 | "require-dev": { 25 | "swoole/ide-helper": "^4.4", 26 | "phpmd/phpmd": "^2.6", 27 | "friendsofphp/php-cs-fixer": "^2.14", 28 | "mockery/mockery": "^1.0", 29 | "doctrine/common": "^2.9", 30 | "phpstan/phpstan": "^0.12", 31 | "hyperf/devtool": "~2.0.0", 32 | "hyperf/testing": "~2.0.0" 33 | }, 34 | "suggest": { 35 | "ext-openssl": "Required to use HTTPS.", 36 | "ext-json": "Required to use JSON.", 37 | "ext-pdo": "Required to use MySQL Client.", 38 | "ext-pdo_mysql": "Required to use MySQL Client.", 39 | "ext-redis": "Required to use Redis Client." 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "App\\": "app/" 44 | }, 45 | "files": [] 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "HyperfTest\\": "./test/" 50 | } 51 | }, 52 | "minimum-stability": "dev", 53 | "prefer-stable": true, 54 | "extra": [], 55 | "scripts": { 56 | "post-root-package-install": [ 57 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 58 | ], 59 | "post-autoload-dump": [ 60 | "rm -rf runtime/container" 61 | ], 62 | "test": "co-phpunit -c phpunit.xml --colors=always", 63 | "cs-fix": "php-cs-fixer fix $1", 64 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", 65 | "start": "php ./bin/hyperf.php start", 66 | "init-proxy": "init-proxy.sh" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'paths' => [ 15 | BASE_PATH . '/app', 16 | ], 17 | 'ignore_annotations' => [ 18 | 'mixin', 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /config/autoload/aspects.php: -------------------------------------------------------------------------------- 1 | StdoutLogger::class, 18 | ]; 19 | -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'http' => [ 15 | Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, 16 | App\Exception\Handler\AppExceptionHandler::class, 17 | ], 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /config/autoload/github.php: -------------------------------------------------------------------------------- 1 | env('GITHUB_USER_AGENT', 'github-bot'), 4 | 'access_token' => env('GITHUB_ACCESS_TOKEN', ''), 5 | 'webhook' => [ 6 | 'secret' => env('GITHUB_WEBHOOK_SECRET', ''), 7 | ], 8 | 'debug' => [ 9 | 'auth' => env('GITHUB_DEBUG_AUTH', ''), 10 | ], 11 | 'merge' => [ 12 | 'method' => env('GITHUB_MERGE_METHOD', 'merge'), 13 | ], 14 | 'comment' => [ 15 | 'header' => "**[This message is created by [huangzhhui/github-bot](https://github.com/huangzhhui/github-bot)]**\r\n\r\n" 16 | ], 17 | ]; -------------------------------------------------------------------------------- /config/autoload/logger.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'handler' => [ 15 | 'class' => Monolog\Handler\StreamHandler::class, 16 | 'constructor' => [ 17 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 18 | 'level' => Monolog\Logger::DEBUG, 19 | ], 20 | ], 21 | 'formatter' => [ 22 | 'class' => Monolog\Formatter\LineFormatter::class, 23 | 'constructor' => [ 24 | 'format' => null, 25 | 'dateFormat' => null, 26 | 'allowInlineLineBreaks' => true, 27 | ], 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | CoroutineServer::class, 19 | 'mode' => SWOOLE_PROCESS, 20 | 'servers' => [ 21 | [ 22 | 'name' => 'http', 23 | 'type' => Server::SERVER_HTTP, 24 | 'host' => '0.0.0.0', 25 | 'port' => 9501, 26 | 'sock_type' => SWOOLE_SOCK_TCP, 27 | 'callbacks' => [ 28 | SwooleEvent::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 29 | ], 30 | ], 31 | ], 32 | 'settings' => [ 33 | 'enable_coroutine' => true, 34 | 'worker_num' => 1, 35 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid', 36 | 'open_tcp_nodelay' => true, 37 | 'max_coroutine' => 100000, 38 | 'open_http2_protocol' => true, 39 | 'max_request' => 100000, 40 | 'socket_buffer_size' => 2 * 1024 * 1024, 41 | 'buffer_output_size' => 2 * 1024 * 1024, 42 | ], 43 | 'callbacks' => [ 44 | SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 45 | SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 46 | SwooleEvent::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 17 | 'app_env' => env('APP_ENV', 'dev'), 18 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 19 | StdoutLoggerInterface::class => [ 20 | 'log_level' => [ 21 | LogLevel::ALERT, 22 | LogLevel::CRITICAL, 23 | LogLevel::DEBUG, 24 | LogLevel::EMERGENCY, 25 | LogLevel::ERROR, 26 | LogLevel::INFO, 27 | LogLevel::NOTICE, 28 | LogLevel::WARNING, 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 25 | $this->assertTrue(is_array($this->get('/'))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 35 | } 36 | 37 | public function __call($name, $arguments) 38 | { 39 | return $this->client->{$name}(...$arguments); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 28 | --------------------------------------------------------------------------------