/** * Generate changelog for this node only * * @param OutputInterface $output * @param LibraryRelease $release * @throws Exception */ protected function generateChangelog(OutputInterface $output, LibraryRelease $release) { // Determine if this library has a changelog configured if (!$release->getLibrary()->hasChangelog()) { return; } // Get from version $fromVersion = $release->getPriorVersion(); if (!$fromVersion) { $this->log($output, "No prior version for library <info>" . $release->getLibrary()->getName() . '</info>, ' . "skipping changelog for initial release"); return; } $this->log($output, "Generating changelog for library <info>" . $release->getLibrary()->getName() . '</info>' . ' (<comment>' . $fromVersion->getValue() . '</comment> to ' . '<info>' . $release->getVersion()->getValue() . '</info>)'); // Given a from version for this release, determine the "from" version for all child dependencies. // This does a deep search through composer dependencies and recursively checks out old versions // of composer.json to determine historic version information $changelogLibrary = $this->getChangelogLibrary($release, $fromVersion); // Preview diffs to generate for this changelog $count = $changelogLibrary->count(); $this->log($output, "Found changes in <info>{$count}</info> modules:"); foreach ($changelogLibrary->getAllItems(true) as $item) { $prior = $item->getPriorVersion()->getValue(); $version = $item->getRelease()->getVersion()->getValue(); $name = $item->getRelease()->getLibrary()->getName(); $this->log($output, " * <info>{$name}</info> from <info>{$prior}</info> to <info>{$version}</info>"); } // Generate markedown from plan $changelog = new Changelog($changelogLibrary); $content = $changelog->getMarkdown($output, $release->getLibrary()->getChangelogFormat()); // Store this changelog $this->storeChangelog($output, $changelogLibrary, $content); }
/** * Update github release notes via github API * * @param OutputInterface $output * @param LibraryRelease $release */ protected function updateGithubChangelog(OutputInterface $output, LibraryRelease $release) { $library = $release->getLibrary(); if (!$library->hasGithubChangelog()) { return; } // Check github slug is available $slug = $release->getLibrary()->getGithubSlug(); if (empty($slug)) { throw new InvalidArgumentException("Could not find github slug for " . $library->getName()); } list($org, $repo) = explode('/', $slug); // Log release $version = $release->getVersion(); $tag = $version->getValue(); $this->log($output, "Creating github release <info>{$org}/{$repo} v{$tag}</info>"); /** @var Repo $reposAPI */ $client = $this->getGithubClient($output); $reposAPI = $client->api('repo'); $releasesAPI = $reposAPI->releases(); // Build release payload // Note target_commitish is omitted since the tag already exists $releaseData = ['tag_name' => $tag, 'name' => $tag, 'prerelease' => !$version->isStable(), 'draft' => false]; $changelog = $release->getChangelog(); if ($changelog) { $releaseData['body'] = $changelog; } // Determine if editing or creating a release $existing = null; try { sleep(1); // Ensure non-zero period between tagging and searching for this tag $existing = $releasesAPI->tag($org, $repo, $tag); } catch (RuntimeException $ex) { if ($ex->getCode() !== 404) { throw $ex; } } // Create or update if ($existing && !empty($existing['id'])) { $result = $releasesAPI->edit($org, $repo, $existing['id'], $releaseData); if (isset($result['created_at'])) { $this->log($output, "Successfully updated github release"); return; } } else { $result = $releasesAPI->create($org, $repo, $releaseData); if (isset($result['created_at'])) { $this->log($output, "Successfully created at <info>" . $result['created_at'] . "</info>"); return; } } $this->log($output, "Github API update failed for <info>" . $library->getName() . "</info>", "error"); }
/** * Serialise a plan to json * * @param LibraryRelease $plan * @return array Encoded json data */ public function serialisePlan(LibraryRelease $plan) { $content = []; $name = $plan->getLibrary()->getName(); $content[$name] = ['Version' => $plan->getVersion()->getValue(), 'Changelog' => $plan->getChangelog(), 'Items' => []]; foreach ($plan->getItems() as $item) { $content[$name]['Items'] = array_merge($content[$name]['Items'], $item->getLibrary()->serialisePlan($item)); } return $content; }
/** * Increment any dependencies on x-dev versions that need updating * * @param OutputInterface $output * @param LibraryRelease $releasePlan */ protected function incrementDevDependencies(OutputInterface $output, LibraryRelease $releasePlan) { $parentLibrary = $releasePlan->getLibrary(); $parentName = $parentLibrary->getName(); $originalData = $composerData = $parentLibrary->getComposerData(); // Inspect all dependencies foreach ($releasePlan->getItems() as $item) { $childName = $item->getLibrary()->getName(); $childVersion = $item->getVersion(); $childConstraint = $parentLibrary->getChildConstraint($childName, $releasePlan->getVersion()); // Compare installed dependency vs version we plan to tag $comparison = $childConstraint->compareTo($childVersion); if (!$comparison) { // Dev dependency is acceptable, no rewrite needed. continue; } // Warn if downgrading constraint, but rewrite anyway. // E.g. installed with 3.1.x-dev, but tagging as 3.0.0 if ($comparison > 0) { $this->log($output, "Warning: Dependency <info>{$childName}</info> of parent <info>{$parentName}</info> " . "was installed with constraint <info>" . $childConstraint->getValue() . "</info> but is " . "being released at a lower version <info>" . $childVersion->getValue() . "</info>", "error"); } // Check if this constraint supports rewriting $newConstraint = $childConstraint->rewriteToSupport($childVersion); if (!$newConstraint || $newConstraint->getValue() === $childConstraint->getValue()) { continue; } $this->log($output, "Rewriting installation constraint of <info>{$childName}</info> from <info>" . $childConstraint->getValue() . "</info> to <info>" . $newConstraint->getValue() . "</info>"); $composerData['require'][$childName] = $newConstraint->getValue(); } // Save modifications to the composer.json for this module if ($composerData !== $originalData) { $this->log($output, "Rewriting composer.json for <info>{$parentName}</info>"); $parentLibrary->setComposerData($composerData); // Commit to git $path = $parentLibrary->getComposerPath(); $repo = $parentLibrary->getRepository(); $repo->run("add", array($path)); $status = $repo->run("status"); if (stripos($status, 'Changes to be committed:')) { $repo->run("commit", array("-m", "Update development dependencies")); } } }
/** * Build user-visible option selection list based on a prepared plan * * @param LibraryRelease $node * @param int $depth * @return array List of options */ protected function getReleaseOptions(LibraryRelease $node, $depth = 0) { $options = []; // Format / indent this line $formatting = str_repeat(' ', $depth) . ($depth ? html_entity_decode('└', ENT_NOQUOTES, 'UTF-8') . ' ' : ''); // Get version release information if ($node->getIsNewRelease()) { $version = ' (<info>' . $node->getVersion()->getValue() . '</info>) new tag'; // If releasing a new tag, show previous version $tags = $node->getLibrary()->getTags(); $previous = $node->getVersion()->getPriorVersionFromTags($tags, $node->getLibrary()->getName()); if ($previous) { $version .= ', prior version <comment>' . $previous->getValue() . '</comment>'; } } else { $version = ' (<comment>' . $node->getVersion()->getValue() . '</comment> existing tag)'; } // Build string $options[$node->getLibrary()->getName()] = $formatting . $node->getLibrary()->getName() . $version; // Build child version options foreach ($node->getItems() as $child) { $options = array_merge($options, $this->getReleaseOptions($child, $depth ? $depth + 3 : 1)); } return $options; }