Bitbucket build status API, create comments on commits and Pull Requests
This commit is contained in:
parent
d15f0f5ef9
commit
d307afa44e
|
@ -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:
|
||||
|
|
|
@ -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' => [
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
108
src/PHPCensor/Helper/Bitbucket.php
Normal file
108
src/PHPCensor/Helper/Bitbucket.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue