Bitbucket build status API, create comments on commits and Pull Requests

This commit is contained in:
StudioMaX 2017-08-20 14:06:51 +06:00
parent d15f0f5ef9
commit d307afa44e
5 changed files with 455 additions and 13 deletions

View file

@ -36,6 +36,12 @@ php-censor:
host: localhost
name: php-censor-queue
lifetime: 600
bitbucket:
username: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app_password: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
comments:
commit: false # This option allow/deny to post comments to Bitbucket commit
pull_request: false # This option allow/deny to post comments to Bitbucket Pull Request
github:
token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
comments:

View file

@ -261,6 +261,14 @@ class InstallCommand extends Command
'smtp_password' => null,
'smtp_encryption' => false,
],
'bitbucket' => [
'username' => null,
'app_password' => null,
'comments' => [
'commit' => false,
'pull_request' => false,
],
],
'github' => [
'token' => null,
'comments' => [

View file

@ -90,21 +90,32 @@ class WebhookController extends Controller
$payload = json_decode(file_get_contents("php://input"), true);
if (empty($payload['push']['changes'])) {
// Invalid event from bitbucket
return [
'status' => 'failed',
'commits' => []
];
// Handle Pull Request webhooks:
if (!empty($payload['pullrequest'])) {
return $this->bitbucketPullRequest($project, $payload);
}
return $this->bitbucketWebhook($payload, $project);
// Handle Push (and Tag) webhooks:
if (!empty($payload['push']['changes'])) {
return $this->bitbucketCommitRequest($project, $payload);
}
// Invalid event from bitbucket
return [
'status' => 'failed',
'commits' => []
];
}
/**
* Bitbucket webhooks.
* Handle the payload when Bitbucket sends a commit webhook.
*
* @param Project $project
* @param array $payload
*
* @return array
*/
protected function bitbucketWebhook($payload, $project)
protected function bitbucketCommitRequest(Project $project, array $payload)
{
$results = [];
$status = 'failed';
@ -134,6 +145,92 @@ class WebhookController extends Controller
return ['status' => $status, 'commits' => $results];
}
/**
* Handle the payload when Bitbucket sends a Pull Request webhook.
*
* @param Project $project
* @param array $payload
*
* @return array
*
* @throws Exception
*/
protected function bitbucketPullRequest(Project $project, array $payload)
{
// We only want to know about open pull requests:
if (!in_array($_SERVER['HTTP_X_EVENT_KEY'], ['pullrequest:created', 'pullrequest:updated'])) {
return ['status' => 'ok'];
}
$headers = [];
$username = Config::getInstance()->get('php-censor.bitbucket.username');
$appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password');
if (empty($username) || empty($appPassword)) {
throw new Exception('Please provide Username and App Password of your Bitbucket account.');
}
$commitsUrl = $payload['pullrequest']['links']['commits']['href'];
$client = new Client();
$commitsResponse = $client->get($commitsUrl, [
'auth' => [$username, $appPassword],
]);
$httpStatus = (integer)$commitsResponse->getStatusCode();
// Check we got a success response:
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new Exception('Could not get commits, failed API request.');
}
$results = [];
$status = 'failed';
$commits = json_decode($commitsResponse->getBody(), true)['values'];
foreach ($commits as $commit) {
// Skip all but the current HEAD commit ID:
$id = $commit['hash'];
if (strpos($id, $payload['pullrequest']['source']['commit']['hash']) !== 0) {
$results[$id] = ['status' => 'ignored', 'message' => 'not branch head'];
continue;
}
try {
$branch = $payload['pullrequest']['destination']['branch']['name'];
$committer = $commit['author']['raw'];
if (strpos($committer, '>') !== false) {
// In order not to loose email if it is RAW, w/o "<>" symbols
$committer = substr($committer, 0, strpos($committer, '>'));
$committer = substr($committer, strpos($committer, '<') + 1);
}
$message = $commit['message'];
$extra = [
'build_type' => 'pull_request',
'pull_request_number' => $payload['pullrequest']['id'],
'remote_branch' => $payload['pullrequest']['source']['branch']['name'],
'remote_reference' => $payload['pullrequest']['source']['repository']['full_name'],
];
$results[$id] = $this->createBuild($project, $id, $branch, null, $committer, $message, $extra);
$status = 'ok';
} catch (Exception $ex) {
$results[$id] = ['status' => 'failed', 'error' => $ex->getMessage()];
}
}
return ['status' => $status, 'commits' => $results];
}
/**
* Bitbucket webhooks.
*
* @deprecated, for BC purpose
*/
protected function bitbucketWebhook($payload, $project)
{
return $this->bitbucketCommitRequest($project, $payload);
}
/**
* Bitbucket POST service.
*/

View file

@ -0,0 +1,108 @@
<?php
namespace PHPCensor\Helper;
use b8\Config;
use GuzzleHttp\Client;
/**
* The Bitbucket Helper class provides some Bitbucket API call functionality.
*/
class Bitbucket
{
/**
* Create a comment on a specific file (and commit) in a Bitbucket Pull Request.
*
* @param string $repo
* @param int $pullId
* @param string $commitId
* @param string $file
* @param int $line
* @param string $comment
*
* @return null
*/
public function createPullRequestComment($repo, $pullId, $commitId, $file, $line, $comment)
{
$username = Config::getInstance()->get('php-censor.bitbucket.username');
$appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password');
if (empty($username) || empty($appPassword)) {
return;
}
$url = '/1.0/repositories/' . $repo . '/pullrequests/' . $pullId . '/comments/';
$client = new Client(['base_uri' => 'https://api.bitbucket.org']);
$response = $client->post($url, [
'auth' => [$username, $appPassword],
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'content' => $comment,
'anchor' => substr($commitId, 0, 12),
'filename' => $file,
'line_to' => $line,
],
]);
}
/**
* Create a comment on a Bitbucket commit.
*
* @param $repo
* @param $commitId
* @param $file
* @param $line
* @param $comment
* @return null
*/
public function createCommitComment($repo, $commitId, $file, $line, $comment)
{
$username = Config::getInstance()->get('php-censor.bitbucket.username');
$appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password');
if (empty($username) || empty($appPassword)) {
return;
}
$url = '/1.0/repositories/' . $repo . '/changesets/' . $commitId . '/comments';
$client = new Client(['base_uri' => 'https://api.bitbucket.org']);
$response = $client->post($url, [
'auth' => [$username, $appPassword],
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'content' => $comment,
'filename' => $file,
'line_to' => $line,
],
]);
}
/**
* @param string $repo
* @param int $pullRequestId
*
* @return string
*/
public function getPullRequestDiff($repo, $pullRequestId)
{
$username = Config::getInstance()->get('php-censor.bitbucket.username');
$appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password');
if (empty($username) || empty($appPassword)) {
return;
}
$url = '/2.0/repositories/' . $repo . '/pullrequests/' . $pullRequestId . '/diff';
$client = new Client(['base_uri' => 'https://api.bitbucket.org']);
$response = $client->get($url, ['auth' => [$username, $appPassword]]);
return (string)$response->getBody();
}
}

View file

@ -2,6 +2,13 @@
namespace PHPCensor\Model\Build;
use GuzzleHttp\Client;
use PHPCensor\Builder;
use PHPCensor\Helper\Bitbucket;
use PHPCensor\Helper\Diff;
use b8\Config;
use PHPCensor\Model\BuildError;
/**
* BitBucket Build Model
*
@ -25,6 +32,91 @@ class BitbucketBuild extends RemoteGitBuild
return 'https://bitbucket.org/' . $this->getProject()->getReference() . '/src/?at=' . $this->getBranch();
}
/**
* Get link to tag from another source (i.e. BitBucket)
*/
public function getTagLink()
{
return 'https://bitbucket.org/' . $this->getProject()->getReference() . '/src/?at=' . $this->getTag();
}
/**
* Send status updates to any relevant third parties (i.e. Bitbucket)
*
* @return bool
*/
public function sendStatusPostback()
{
if ('Manual' === $this->getCommitId()) {
return false;
}
$project = $this->getProject();
if (empty($project)) {
return false;
}
$username = Config::getInstance()->get('php-censor.bitbucket.username');
$appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password');
if (empty($username) || empty($appPassword) || empty($this->data['id'])) {
return false;
}
switch ($this->getStatus()) {
case 0:
case 1:
$status = 'INPROGRESS';
$description = 'PHP Censor build running.';
break;
case 2:
$status = 'SUCCESSFUL';
$description = 'PHP Censor build passed.';
break;
case 3:
$status = 'FAILED';
$description = 'PHP Censor build failed.';
break;
default:
$status = 'STOPPED';
$description = 'PHP Censor build failed to complete.';
break;
}
$phpCensorUrl = Config::getInstance()->get('php-censor.url');
$url = sprintf(
'/2.0/repositories/%s/commit/%s/statuses/build',
$this->getExtra('build_type') == 'pull_request'
? $this->getExtra('remote_reference')
: $project->getReference(),
$this->getCommitId()
);
$client = new Client([
'base_uri' => 'https://api.bitbucket.org',
'http_errors' => false,
]);
$response = $client->post($url, [
'auth' => [$username, $appPassword],
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'state' => $status,
'key' => 'PHP-CENSOR',
'url' => $phpCensorUrl . '/build/view/' . $this->getId(),
'name' => 'PHP Censor Build #' . $this->getId(),
'description' => $description,
],
]);
$status = (integer)$response->getStatusCode();
return ($status >= 200 && $status < 300);
}
/**
* Get the URL to be used to clone this remote repository.
*/
@ -49,10 +141,7 @@ class BitbucketBuild extends RemoteGitBuild
$reference = $this->getProject()->getReference();
if ($this->getExtra('build_type') == 'pull_request') {
$matches = [];
preg_match('/[\/:]([a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-]+)/', $this->getExtra('remote_url'), $matches);
$reference = $matches[1];
$reference = $this->getExtra('remote_reference');
}
$link = 'https://bitbucket.org/' . $reference . '/';
@ -62,4 +151,138 @@ class BitbucketBuild extends RemoteGitBuild
return $link;
}
/**
* Handle any post-clone tasks, like applying a pull request patch on top of the branch.
* @param Builder $builder
* @param $cloneTo
* @param array $extra
* @return bool
*/
protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = null)
{
$buildType = $this->getExtra('build_type');
$success = true;
$skipGitFinalization = false;
try {
if (!empty($buildType) && $buildType == 'pull_request') {
$helper = new Bitbucket();
$diff = $helper->getPullRequestDiff(
$this->getProject()->getReference(),
$this->getExtra('pull_request_number')
);
$diffFile = $this->writeDiff($builder->buildPath, $diff);
$cmd = 'cd "%s" && git checkout -b php-censor/' . $this->getId() . ' && git apply "%s"';
$success = $builder->executeCommand($cmd, $cloneTo, $diffFile);
unlink($diffFile);
$skipGitFinalization = true;
}
} catch (\Exception $ex) {
$success = false;
}
if ($success && !$skipGitFinalization) {
$success = parent::postCloneSetup($builder, $cloneTo, $extra);
}
return $success;
}
/**
* Create an diff file on disk for this build.
*
* @param string $cloneTo
*
* @return string
*/
protected function writeDiff($cloneTo, $diff)
{
$filePath = dirname($cloneTo . '/temp');
$diffFile = $filePath . '.patch';
file_put_contents($diffFile, $diff);
chmod($diffFile, 0600);
return $diffFile;
}
/**
* @inheritDoc
*/
public function reportError(
Builder $builder,
$plugin,
$message,
$severity = BuildError::SEVERITY_NORMAL,
$file = null,
$lineStart = null,
$lineEnd = null
) {
$allowCommentCommit = (boolean)Config::getInstance()->get('php-censor.bitbucket.comments.commit', false);
$allowCommentPullRequest = (boolean)Config::getInstance()->get('php-censor.bitbucket.comments.pull_request', false);
//$file = $builder->buildPath.'test.php';
if ($allowCommentCommit || $allowCommentPullRequest) {
$diffLineNumber = $this->getDiffLineNumber($builder, $file, $lineStart);
if (!is_null($diffLineNumber)) {
$helper = new Bitbucket();
$repo = $this->getProject()->getReference();
$prNumber = $this->getExtra('pull_request_number');
$commit = $this->getCommitId();
if (!empty($prNumber)) {
if ($allowCommentPullRequest) {
$helper->createPullRequestComment($repo, $prNumber, $commit, $file, $lineStart, $message);
}
} else {
if ($allowCommentCommit) {
$helper->createCommitComment($repo, $commit, $file, $lineStart, $message);
}
}
}
}
parent::reportError($builder, $plugin, $message, $severity, $file, $lineStart, $lineEnd);
}
/**
* Uses git diff to figure out what the diff line position is, based on the error line number.
* @param Builder $builder
* @param $file
* @param $line
* @return int|null
*/
protected function getDiffLineNumber(Builder $builder, $file, $line)
{
$line = (integer)$line;
$builder->logExecOutput(false);
$prNumber = $this->getExtra('pull_request_number');
$path = $builder->buildPath;
if (!empty($prNumber)) {
$builder->executeCommand('cd %s && git diff origin/%s "%s"', $path, $this->getBranch(), $file);
} else {
$commitId = $this->getCommitId();
$compare = $commitId == 'Manual' ? 'HEAD' : $commitId;
$builder->executeCommand('cd %s && git diff %s^^ "%s"', $path, $compare, $file);
}
$builder->logExecOutput(true);
$diff = $builder->getLastOutput();
$helper = new Diff();
$lines = $helper->getLinePositions($diff);
return isset($lines[$line]) ? $lines[$line] : null;
}
}