diff --git a/RoboFile.php b/RoboFile.php index 94fd30cc99..bc7687d2b1 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -588,6 +588,16 @@ class RoboFile extends \Robo\Tasks { ); } + protected function getReleaseVersionController($project) { + require_once './tasks/release/ReleaseVersionController.php'; + $this->loadEnv(); + return \MailPoetTasks\Release\ReleaseVersionController::createWithJiraCredentials( + getenv('WP_JIRA_TOKEN'), + getenv('WP_JIRA_USER'), + $project + ); + } + public function testAcceptanceGroupTests() { return $this->taskSplitTestFilesByGroups(4) ->projectRoot('.') @@ -598,10 +608,7 @@ class RoboFile extends \Robo\Tasks { public function writeReleaseVersion($version) { $version = trim($version); - if (!preg_match('/\d+\.\d+\.\d+/', $version)) { - $this->yell('Incorrect version format', 40, 'red'); - exit(1); - } + $this->validateVersion($version); $this->taskReplaceInFile(__DIR__ . '/readme.txt') ->regex('/Stable tag:\s*\d+\.\d+\.\d+/i') @@ -618,4 +625,34 @@ class RoboFile extends \Robo\Tasks { ->to(sprintf("'version' => '%s',", $version)) ->run(); } + + public function jiraReleaseVersion($opts = ['free' => null, 'premium' => null]) { + require_once './tasks/release/Jira.php'; + if (empty($opts['free']) && empty($opts['premium'])) { + $this->yell('No Free or Premium version specified', 40, 'red'); + exit(1); + } + $output = []; + if (!empty($opts['free'])) { + $this->validateVersion($opts['free']); + $output[] = $this->getReleaseVersionController(\MailPoetTasks\Release\Jira::PROJECT_MAILPOET) + ->assignVersionToCompletedTickets($opts['free']); + } + if (!empty($opts['premium'])) { + $this->validateVersion($opts['premium']); + $output[] = $this->getReleaseVersionController(\MailPoetTasks\Release\Jira::PROJECT_PREMIUM) + ->assignVersionToCompletedTickets($opts['premium']); + } + if($opts['quiet']) { + return; + } + $this->say(join("\n", $output)); + } + + private function validateVersion($version) { + if (!preg_match('/\d+\.\d+\.\d+/', $version)) { + $this->yell('Incorrect version format', 40, 'red'); + exit(1); + } + } } diff --git a/tasks/release/Jira.php b/tasks/release/Jira.php index 879d26cea8..8e0bc47df4 100644 --- a/tasks/release/Jira.php +++ b/tasks/release/Jira.php @@ -11,6 +11,7 @@ class Jira { const PROJECT_PREMIUM = 'PREMIUM'; const JIRA_DOMAIN = 'mailpoet.atlassian.net'; + const JIRA_API_VERSION = '3'; /** @var string */ private $token; @@ -31,7 +32,7 @@ class Jira { * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-project-projectIdOrKey-versions-get */ function getVersion($version_name = null) { - $versions = $this->fetchFromJira("project/$this->project/versions"); + $versions = $this->makeJiraRequest("project/$this->project/versions"); if ($version_name === null) { return end($versions); } @@ -44,12 +45,22 @@ class Jira { } /** - * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-search-get + * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-version-post */ + function createVersion($version_name) { + $data = [ + 'name' => $version_name, + 'archived' => false, + 'released' => false, + 'project' => $this->project, + ]; + return $this->makeJiraRequest('/version', 'POST', $data); + } + function getIssuesDataForVersion($version) { $changelog_id = self::CHANGELOG_FIELD_ID; $release_note_id = self::RELEASENOTE_FIELD_ID; - $issues_data = $this->fetchFromJira("/search?fields=key,$changelog_id,$release_note_id,status&jql=fixVersion={$version['id']}"); + $issues_data = $this->search("fixVersion={$version['id']}", ['key', $changelog_id, $release_note_id, 'status']); // Sort issues by importance of change (Added -> Updated -> Improved -> Changed -> Fixed -> Others) usort($issues_data['issues'], function($a, $b) use ($changelog_id) { $order = array_flip(['added', 'updat', 'impro', 'chang', 'fixed']); @@ -62,16 +73,46 @@ class Jira { return $issues_data['issues']; } - private function fetchFromJira($path) { + /** + * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-search-get + */ + function search($jql, array $fields = null) { + $params = ['jql' => $jql]; + if ($fields) { + $params['fields'] = join(',', $fields); + } + return $this->makeJiraRequest("/search?" . http_build_query($params)); + } + + /** + * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-put + */ + function updateIssue($key, $data) { + return $this->makeJiraRequest("/issue/$key", 'PUT', $data); + } + + private function makeJiraRequest($path, $method = 'GET', array $data = null) { $url_user = urlencode($this->user); $url_token = urlencode($this->token); $jira_domain = self::JIRA_DOMAIN; - $jira_url = "https://$url_user:$url_token@$jira_domain/rest/api/3/$path"; - $data = file_get_contents($jira_url); - if ($data === false) { + $jira_api_version = self::JIRA_API_VERSION; + $jira_url = "https://$url_user:$url_token@$jira_domain/rest/api/$jira_api_version/$path"; + $options = []; + if ($method === 'POST' || $method === 'PUT') { + $options = [ + 'http' => [ + 'method' => $method, + 'header' => "Content-type: application/json\r\n", + 'content' => json_encode($data), + ] + ]; + } + $context = stream_context_create($options); + $result = file_get_contents($jira_url, false, $context); + if ($result === false) { $error = error_get_last(); throw new \Exception('JIRA request error: ' . $error['message']); } - return json_decode($data, true); + return json_decode($result, true); } } diff --git a/tasks/release/ReleaseVersionController.php b/tasks/release/ReleaseVersionController.php new file mode 100644 index 0000000000..ca65768ed5 --- /dev/null +++ b/tasks/release/ReleaseVersionController.php @@ -0,0 +1,81 @@ +jira = $jira; + $this->project = $project; + } + + static function createWithJiraCredentials($token, $user, $project) { + return new self(new Jira($token, $user, $project), $project); + } + + function assignVersionToCompletedTickets($version) { + $output = []; + $output[] = "Checking version $version in $this->project"; + + if (!$this->checkVersion($version)) { + $output[] = "The version is invalid or already released"; + return join("\n", $output); + } + + $output[] = "Setting version $version to completed tickets in $this->project..."; + $issues = $this->getDoneIssuesWithoutVersion(); + $result = array_map(function ($issue) use ($version) { + return $this->setIssueFixVersion($issue['key'], $version); + }, $issues); + $output[] = "Done, issues processed: " . count($result); + + return join("\n", $output); + } + + function getDoneIssuesWithoutVersion() { + $jql = "project = $this->project AND status = Done AND (fixVersion = EMPTY OR fixVersion IN unreleasedVersions()) AND updated >= -52w"; + $result = $this->jira->search($jql, ['key']); + return array_map(function ($issue) { + return [ + 'id' => $issue['id'], + 'key' => $issue['key'], + ]; + }, $result['issues']); + } + + function checkVersion($version) { + try { + $version_data = $this->jira->getVersion($version); + } catch (\Exception $e) { + $version_data = false; + } + if (!empty($version_data['released'])) { + // version is already released + return false; + } else if (empty($version_data)) { + // version does not exist + $this->jira->createVersion($version); + } + // version exists + return true; + } + + function setIssueFixVersion($issue_key, $version) { + $data = [ + 'update' => [ + 'fixVersions' => [ + ['set' => [['name' => $version]]] + ] + ] + ]; + return $this->jira->updateIssue($issue_key, $data); + } +}