diff --git a/release-notes.php b/release-notes.php new file mode 100755 index 0000000000000000000000000000000000000000..0dae5d3b576abbf0d9564287ceba3a9636ac1f7c --- /dev/null +++ b/release-notes.php @@ -0,0 +1,18 @@ +#!/usr/bin/env php + $ref, + ]; + $result = uw_wcms_tools_query_gitlab_api('projects/' . $id . '/repository/files/' . $path, $query); + + if (isset($result['body']->content)) { + $result['body']->content = base64_decode($result['body']->content); + } + + return $result; +} + +/** + * List commits in a GitLab repository. + * + * @param string $project + * The repository to load from. + * @param string $start + * The ref to load commits from or the start of the series of commits to load. + * @param string $end + * The end of the series of commits to load. + * + * @return array + * The GitLab API response. + */ +function uw_wcms_tools_gitlab_get_commits(string $project, string $start, string $end = NULL): array { + $ref_name = $start; + if ($end) { + $ref_name .= '..' . $end; + } + + $id = urlencode($project); + $query = [ + 'ref_name' => $ref_name, + 'per_page' => 100, + ]; + return uw_wcms_tools_query_gitlab_api('projects/' . $id . '/repository/commits', $query); +} + +/** + * Add commits between two revisions to an array of projects. + * + * @param array $projects + * An array of arrays of revisions keyed by project name. + */ +function uw_wcms_tools_gitlab_get_project_commits(array &$projects): void { + foreach ($projects as $name => $versions) { + // Exclude non-WCMS projects. + list($group) = explode('/', $name, 2); + if ($group !== 'wcms') { + // Indicate that lookup was not attempted. + $projects[$name]['commits'] = NULL; + continue; + } + + $projects[$name]['commits'] = []; + + // Exclude projects that are not upgrades. + if (!$versions['start'] || !$versions['end'] || $versions['start'] === $versions['end']) { + continue; + } + + $commits = uw_wcms_tools_gitlab_get_commits($name, $versions['start'], $versions['end']); + if ($commits['http_status'] !== 200) { + throw new Exception('Unable to load commits.'); + } + foreach ($commits['body'] as $commit) { + // @todo Replace with str_starts_with() after upgrade to PHP 8. + if (strncmp($commit->title, 'Merge branch', 12) === 0) { + continue; + } + + $projects[$name]['commits'][] = $commit; + } + } +} diff --git a/uw_wcms_tools.jira.inc b/uw_wcms_tools.jira.inc index 42920ca8bd4615889cdf09f97309f1cdaf135648..0d34da48bddfa13480baa5a7f60385c35b867ebc 100644 --- a/uw_wcms_tools.jira.inc +++ b/uw_wcms_tools.jira.inc @@ -9,6 +9,28 @@ */ define('UW_WCMS_TOOLS_JIRA_URL_PREFIX', 'https://jira.uwaterloo.ca/browse/'); +define('UW_WCMS_TOOLS_JIRA_API_LOCATION', 'https://jira.uwaterloo.ca/rest/api/2/'); + +/** + * Return the Jira Personal Access Token (PAT). + * + * Call variable_get() if it exists, otherwise use environment variables. + * + * @return string + * The token. + */ +function uw_wcms_tools_get_jira_credentials(): string { + if (function_exists('variable_get')) { + $jira_access_token = variable_get('jira_access_token'); + } + else { + $jira_access_token = getenv('JIRA_ACCESS_TOKEN'); + } + if (!$jira_access_token) { + throw new Exception('Jira API credentials not configured.'); + } + return $jira_access_token; +} /** * Run a query on the Jira API. @@ -28,27 +50,26 @@ function uw_wcms_tools_jira_api_query($path, stdClass $data = NULL, $method = 'P throw new Exception('Unsupported HTTP method.'); } - $jira_username = variable_get('jira_username'); - $jira_password = variable_get('jira_password'); - if (!$jira_username || !$jira_password) { - throw new Exception('Jira API credentials not configured.'); - } + // Method is GET unless there is data. + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => [ + 'Authorization: Bearer ' . uw_wcms_tools_get_jira_credentials(), + ], + ], + ]; if ($data) { - $opts = [ - 'http' => [ - 'method' => $method, - 'header' => 'Content-Type: application/json', - 'content' => json_encode($data), - ], - ]; - $context = stream_context_create($opts); - } - else { - $context = NULL; + $options['http']['method'] = $method; + $options['http']['header'][] = 'Content-Type: application/json'; + $options['http']['content'] = json_encode($data); } - $url = 'https://' . $jira_username . ':' . $jira_password . '@jira.uwaterloo.ca/rest/api/2/' . $path; + $options['http']['header'] = implode("\r\n", $options['http']['header']); + $context = stream_context_create($options); + + $url = UW_WCMS_TOOLS_JIRA_API_LOCATION . $path; $results = file_get_contents($url, FALSE, $context); if ($results) { return json_decode($results); diff --git a/uw_wcms_tools.releasenotes.inc b/uw_wcms_tools.releasenotes.inc new file mode 100644 index 0000000000000000000000000000000000000000..432328ee693220bd5dd3bd45b29da5d93256da12 --- /dev/null +++ b/uw_wcms_tools.releasenotes.inc @@ -0,0 +1,124 @@ + $start, + 'end' => $end, + ]; + + // Load uw_base_profile composer.json from the start and end revision. Make an + // array of required project names and the version of each at the start and + // end revisions. + $projects = []; + foreach (array_keys($tags) as $stage) { + $composer = uw_wcms_tools_gitlab_get_file('wcms/uw_base_profile', 'composer.json', $tags[$stage]); + if ($composer['http_status'] !== 200) { + throw new Exception('Unable to load composer.json.'); + } + $composer = json_decode($composer['body']->content); + + foreach ($composer->require as $name => $version) { + $projects[$name][$stage] = $version; + } + } + + // Generate a list of projects that have been updated. + $release_notes = "Projects updated\n"; + foreach ($projects as $name => &$versions) { + foreach (array_keys($tags) as $stage) { + $versions[$stage] = $versions[$stage] ?? NULL; + } + + if ($versions['start'] === $versions['end']) { + continue; + } + + $release_notes .= sprintf('%-40s: ', $name); + + if (!$versions['start']) { + $release_notes .= 'Added'; + } + elseif (!$versions['end']) { + $release_notes .= 'Removed'; + } + else { + $release_notes .= sprintf('Upgraded from %s to %s', $versions['start'], $versions['end']); + } + + $release_notes .= "\n"; + } + + $release_notes .= "\n"; + + uw_wcms_tools_gitlab_get_project_commits($projects); + + // Generate a list of updated WCMS projects and the commits each has had. + $release_notes .= "Updates to WCMS projects\n"; + foreach ($projects as $path => $commits) { + // Skip projects with no commits. + if (!$commits['commits']) { + continue; + } + list(, $project) = explode('/', $path, 2); + $release_notes .= $project . "\n"; + foreach ($commits['commits'] as $commit) { + $release_notes .= "\t" . $commit->title . "\n"; + } + } + + $release_notes .= "\n"; + + // Generate a list of Jira tickets acted on. + $release_notes .= "Tickets acted on\n"; + $tickets = []; + // Make array with keys being the ticket numbers from two-dimensional array of + // commits. + foreach ($projects as $path => $commits) { + if (!$commits['commits']) { + continue; + } + foreach ($commits['commits'] as $commit) { + if (preg_match('/ISTWCMS-\d+/', $commit->title, $matches)) { + foreach ($matches as $match) { + $tickets[$match] = TRUE; + } + } + } + } + $tickets = array_keys($tickets); + // Sort tickets and output with titles from Jira. + sort($tickets); + $use_jira = TRUE; + foreach ($tickets as $ticket) { + $release_notes .= $ticket; + // If Jira API is available, show ticket titles. On failure, do not re-try. + if ($use_jira) { + $jira_ticket = @uw_wcms_tools_jira_api_query('issue/' . $ticket); + if (isset($jira_ticket->fields->summary)) { + $release_notes .= ': ' . $jira_ticket->fields->summary; + } + else { + $use_jira = FALSE; + } + } + $release_notes .= "\n"; + } + + return $release_notes; +}