diff --git a/src/Action/DetermineNextRelease.php b/src/Action/DetermineNextRelease.php index e220634b4..c3538f220 100644 --- a/src/Action/DetermineNextRelease.php +++ b/src/Action/DetermineNextRelease.php @@ -16,7 +16,7 @@ use App\Action\Exception\CannotDetermineNextRelease; use App\Action\Exception\NoPullRequestsMergedSinceLastRelease; use App\Command\AbstractCommand; -use App\Domain\Exception\NoBranchesAvailable; +use App\Domain\Value\Branch; use App\Domain\Value\NextRelease; use App\Domain\Value\Project; use App\Github\Api\Branches; @@ -49,24 +49,16 @@ public function __construct( $this->pullRequests = $pullRequests; } - public function __invoke(Project $project): NextRelease + public function __invoke(Project $project, Branch $branch): NextRelease { $repository = $project->repository(); try { - $branch = $project->stableBranch() ?? $project->unstableBranch(); - } catch (NoBranchesAvailable $e) { - throw CannotDetermineNextRelease::forProject( - $project, - $e - ); - } - - try { - $currentRelease = $this->releases->latest($repository); + $currentRelease = $this->releases->latestForBranch($repository, $branch); } catch (LatestReleaseNotFound $e) { - throw CannotDetermineNextRelease::forProject( + throw CannotDetermineNextRelease::forBranch( $project, + $branch, $e ); } @@ -77,8 +69,9 @@ public function __invoke(Project $project): NextRelease ); if ([] === $pullRequests) { - throw NoPullRequestsMergedSinceLastRelease::forProject( + throw NoPullRequestsMergedSinceLastRelease::forBranch( $project, + $branch, $currentRelease->publishedAt() ); } @@ -100,6 +93,7 @@ public function __invoke(Project $project): NextRelease return NextRelease::fromValues( $project, + $branch, $currentRelease->tag(), $combinedStatus, $checkRuns, diff --git a/src/Action/DetermineNextReleaseVersion.php b/src/Action/DetermineNextReleaseVersion.php index d1e44ca52..14c5bcf80 100644 --- a/src/Action/DetermineNextReleaseVersion.php +++ b/src/Action/DetermineNextReleaseVersion.php @@ -32,7 +32,17 @@ public static function forTagAndPullRequests(Tag $current, array $pullRequests): return $pr->stability()->toString(); }, $pullRequests); - $parts = explode('.', $current->toString()); + // Add compatibility for non-semantical versioning version like `4.0.0-alpha-1` + $currentTag = str_replace('-', '.', $current->toString()); + $parts = explode('.', $currentTag); + + if (isset($parts[3])) { + return Tag::fromString(implode('.', [$parts[0], $parts[1], $parts[2]])); + } + + if (\in_array(Stability::major()->toString(), $stabilities, true)) { + return Tag::fromString(implode('.', [(int) $parts[0] + 1, 0, 0])); + } if (\in_array(Stability::minor()->toString(), $stabilities, true)) { return Tag::fromString(implode('.', [$parts[0], (int) $parts[1] + 1, 0])); diff --git a/src/Action/Exception/CannotDetermineNextRelease.php b/src/Action/Exception/CannotDetermineNextRelease.php index 48140906d..5eaf02f9c 100644 --- a/src/Action/Exception/CannotDetermineNextRelease.php +++ b/src/Action/Exception/CannotDetermineNextRelease.php @@ -13,6 +13,7 @@ namespace App\Action\Exception; +use App\Domain\Value\Branch; use App\Domain\Value\Project; /** @@ -20,11 +21,12 @@ */ final class CannotDetermineNextRelease extends \RuntimeException { - public static function forProject(Project $project, ?\Throwable $previous = null): self + public static function forBranch(Project $project, Branch $branch, ?\Throwable $previous = null): self { return new self( sprintf( - 'Cannot determine next release for Project "%s".', + 'Cannot determine next release for branch "%s" of project "%s".', + $branch->name(), $project->name() ), 0, diff --git a/src/Action/Exception/NoPullRequestsMergedSinceLastRelease.php b/src/Action/Exception/NoPullRequestsMergedSinceLastRelease.php index 1be3f0c75..763011e1f 100644 --- a/src/Action/Exception/NoPullRequestsMergedSinceLastRelease.php +++ b/src/Action/Exception/NoPullRequestsMergedSinceLastRelease.php @@ -13,6 +13,7 @@ namespace App\Action\Exception; +use App\Domain\Value\Branch; use App\Domain\Value\Project; /** @@ -20,12 +21,13 @@ */ final class NoPullRequestsMergedSinceLastRelease extends \RuntimeException { - public static function forProject(Project $project, \DateTimeImmutable $lastRelease, ?\Throwable $previous = null): self + public static function forBranch(Project $project, Branch $branch, \DateTimeImmutable $lastRelease, ?\Throwable $previous = null): self { return new self( sprintf( - 'No pull requests merged since last release "%s" for Project "%s".', + 'No pull requests merged since last release "%s" for branch "%s" of project "%s".', $lastRelease->format('Y-m-d H:i:s'), + $branch->name(), $project->name() ), 0, diff --git a/src/Command/ReleaseCommand.php b/src/Command/ReleaseCommand.php index ac5348a7f..02960286c 100644 --- a/src/Command/ReleaseCommand.php +++ b/src/Command/ReleaseCommand.php @@ -17,6 +17,7 @@ use App\Action\Exception\CannotDetermineNextRelease; use App\Action\Exception\NoPullRequestsMergedSinceLastRelease; use App\Config\Projects; +use App\Domain\Value\Branch; use App\Domain\Value\Project; use App\Domain\Value\Stability; use App\Github\Domain\Value\CheckRun; @@ -27,6 +28,7 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; /** @@ -83,10 +85,11 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $project = $this->selectProject($input, $output); + $branch = $this->selectBranch($input, $output, $project); $this->io->getErrorStyle()->title($project->name()); - return $this->renderNextRelease($project); + return $this->renderNextRelease($project, $branch); } private function selectProject(InputInterface $input, OutputInterface $output): Project @@ -95,9 +98,7 @@ private function selectProject(InputInterface $input, OutputInterface $output): $question = new Question('Please enter the name of the project to release: '); $question->setAutocompleterValues(array_keys($this->projects->all())); - $question->setNormalizer(static function ($answer) { - return $answer ? trim($answer) : ''; - }); + $question->setTrimmable(true); $question->setValidator(function ($answer): Project { return $this->projects->byName($answer); }); @@ -106,11 +107,31 @@ private function selectProject(InputInterface $input, OutputInterface $output): return $helper->ask($input, $output, $question); } - private function renderNextRelease(Project $project): int + private function selectBranch(InputInterface $input, OutputInterface $output, Project $project): Branch + { + $helper = $this->getHelper('question'); + + $default = ($project->stableBranch() ?? $project->unstableBranch())->name(); + + $question = new ChoiceQuestion( + sprintf('Please select the branch of the project to release: (Default: "%s")', $default), + $project->branchNamesReverse(), + $default + ); + $question->setTrimmable(true); + $question->setValidator(static function ($answer) use ($project): Branch { + return $project->branch($answer); + }); + $question->setMaxAttempts(3); + + return $helper->ask($input, $output, $question); + } + + private function renderNextRelease(Project $project, Branch $branch): int { $notificationStyle = $this->io->getErrorStyle(); try { - $nextRelease = $this->determineNextRelease->__invoke($project); + $nextRelease = $this->determineNextRelease->__invoke($project, $branch); } catch (NoPullRequestsMergedSinceLastRelease $e) { $notificationStyle->warning($e->getMessage()); diff --git a/src/Config/Exception/UnknownBranch.php b/src/Config/Exception/UnknownBranch.php new file mode 100644 index 000000000..4328f3243 --- /dev/null +++ b/src/Config/Exception/UnknownBranch.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Config\Exception; + +use App\Domain\Value\Project; + +/** + * @author Oskar Stark + */ +final class UnknownBranch extends \InvalidArgumentException +{ + public static function forName(Project $project, string $name): self + { + return new self(sprintf( + 'Could not find branch with name "%s" for project "%s".', + $name, + $project->name() + )); + } +} diff --git a/src/Config/Exception/UnknownProject.php b/src/Config/Exception/UnknownProject.php index db6460494..092c31a01 100644 --- a/src/Config/Exception/UnknownProject.php +++ b/src/Config/Exception/UnknownProject.php @@ -21,7 +21,7 @@ final class UnknownProject extends \InvalidArgumentException public static function forName(string $name): self { return new self(sprintf( - 'Could not find Project with name "%s".', + 'Could not find project with name "%s".', $name )); } diff --git a/src/Controller/NextReleaseForProjectController.php b/src/Controller/NextReleaseForProjectController.php index d9b8c9512..97289a6d2 100644 --- a/src/Controller/NextReleaseForProjectController.php +++ b/src/Controller/NextReleaseForProjectController.php @@ -36,17 +36,18 @@ public function __construct(Projects $projects, DetermineNextRelease $determineN } /** - * @Route("/next-release/{projectName}", name="next_release_project") + * @Route("/next-release/{projectName}/{branchName}", name="next_release_project") */ - public function __invoke(string $projectName): Response + public function __invoke(string $projectName, string $branchName): Response { try { $project = $this->projects->byName($projectName); + $branch = $project->branch($branchName); } catch (UnknownProject $e) { throw new NotFoundHttpException($e->getMessage()); } - $release = $this->determineNextRelease->__invoke($project); + $release = $this->determineNextRelease->__invoke($project, $branch); $content = $this->twig->render( 'releases/project.html.twig', diff --git a/src/Controller/NextReleaseOverviewController.php b/src/Controller/NextReleaseOverviewController.php index 3f80d4a10..672fc001d 100644 --- a/src/Controller/NextReleaseOverviewController.php +++ b/src/Controller/NextReleaseOverviewController.php @@ -42,13 +42,19 @@ public function __construct(Projects $projects, DetermineNextRelease $determineN public function __invoke(): Response { $releases = array_reduce($this->projects->all(), function (array $releases, Project $project): array { - try { - $release = $this->determineNextRelease->__invoke($project); - } catch (CannotDetermineNextRelease | NoPullRequestsMergedSinceLastRelease $e) { - return $releases; - } + foreach ($project->branches() as $branch) { + if ('master' === $branch->name() && $project->isStable()) { + continue; + } + + try { + $release = $this->determineNextRelease->__invoke($project, $branch); + } catch (CannotDetermineNextRelease | NoPullRequestsMergedSinceLastRelease $e) { + continue; + } - $releases[] = $release; + $releases[] = $release; + } return $releases; }, []); diff --git a/src/Domain/Value/NextRelease.php b/src/Domain/Value/NextRelease.php index 0029109e4..67e9529e5 100644 --- a/src/Domain/Value/NextRelease.php +++ b/src/Domain/Value/NextRelease.php @@ -25,6 +25,7 @@ final class NextRelease { private Project $project; + private Branch $branch; private Tag $currentTag; private CombinedStatus $combinedStatus; private CheckRuns $checkRuns; @@ -38,12 +39,14 @@ final class NextRelease private function __construct( Project $project, + Branch $branch, Tag $currentTag, CombinedStatus $combinedStatus, CheckRuns $checkRuns, array $pullRequests ) { $this->project = $project; + $this->branch = $branch; $this->currentTag = $currentTag; $this->combinedStatus = $combinedStatus; @@ -69,6 +72,7 @@ private function __construct( */ public static function fromValues( Project $project, + Branch $branch, Tag $currentTag, CombinedStatus $combinedStatus, CheckRuns $checkRuns, @@ -76,6 +80,7 @@ public static function fromValues( ): self { return new self( $project, + $branch, $currentTag, $combinedStatus, $checkRuns, @@ -88,6 +93,11 @@ public function project(): Project return $this->project; } + public function branch(): Branch + { + return $this->branch; + } + public function currentTag(): Tag { return $this->currentTag; @@ -185,6 +195,10 @@ public function stability(): Stability return $pr->stability()->toString(); }, $this->pullRequests); + if (\in_array(Stability::major()->toString(), $stabilities, true)) { + return Stability::major(); + } + if (\in_array(Stability::minor()->toString(), $stabilities, true)) { return Stability::minor(); } diff --git a/src/Domain/Value/Project.php b/src/Domain/Value/Project.php index c9d66dec9..2a36ca177 100644 --- a/src/Domain/Value/Project.php +++ b/src/Domain/Value/Project.php @@ -13,6 +13,7 @@ namespace App\Domain\Value; +use App\Config\Exception\UnknownBranch; use App\Domain\Exception\NoBranchesAvailable; use Packagist\Api\Result\Package; use function Symfony\Component\String\u; @@ -133,6 +134,17 @@ public function package(): Package return $this->package; } + public function branch(string $name): Branch + { + foreach ($this->branches as $branch) { + if ($branch->name() === $name) { + return $branch; + } + } + + throw UnknownBranch::forName($this, $name); + } + /** * @return Branch[] */ @@ -305,6 +317,11 @@ public function stableBranch(): ?Branch return $this->branches[1] ?? null; } + public function isStable(): bool + { + return null !== $this->stableBranch(); + } + private function getLatestPackagistVersion(): Package\Version { $versions = $this->package->getVersions(); diff --git a/src/Domain/Value/Stability.php b/src/Domain/Value/Stability.php index 6e38962ec..0d56fbcb8 100644 --- a/src/Domain/Value/Stability.php +++ b/src/Domain/Value/Stability.php @@ -32,6 +32,7 @@ private function __construct(string $value) Assert::oneOf( $value, [ + 'major', 'minor', 'patch', 'pedantic', @@ -47,6 +48,11 @@ public static function fromString(string $value): self return new self($value); } + public static function major(): self + { + return new self('major'); + } + public static function minor(): self { return new self('minor'); diff --git a/src/Github/Api/Releases.php b/src/Github/Api/Releases.php index a086d7991..47a36ee0a 100644 --- a/src/Github/Api/Releases.php +++ b/src/Github/Api/Releases.php @@ -13,6 +13,7 @@ namespace App\Github\Api; +use App\Domain\Value\Branch; use App\Domain\Value\Repository; use App\Github\Domain\Value\Release; use App\Github\Exception\LatestReleaseNotFound; @@ -47,4 +48,28 @@ public function latest(Repository $repository): Release return Release::fromResponse($response); } + + public function latestForBranch(Repository $repository, Branch $branch): Release + { + try { + $response = $this->github->repo()->releases()->all( + $repository->username(), + $repository->name() + ); + } catch (RuntimeException $e) { + throw LatestReleaseNotFound::forRepositoryAndBranch( + $repository, + $branch, + $e + ); + } + + foreach ($response as $release) { + if ($branch->name() === $release['target_commitish']) { + return Release::fromResponse($release); + } + } + + throw LatestReleaseNotFound::forRepositoryAndBranch($repository, $branch); + } } diff --git a/src/Github/Domain/Value/PullRequest.php b/src/Github/Domain/Value/PullRequest.php index 4b80a8f7c..91ddc3286 100644 --- a/src/Github/Domain/Value/PullRequest.php +++ b/src/Github/Domain/Value/PullRequest.php @@ -227,6 +227,10 @@ public function stability(): Stability return $label->name(); }, $this->labels); + if (\in_array('major', $labels, true)) { + return Stability::major(); + } + if (\in_array('minor', $labels, true)) { return Stability::minor(); } diff --git a/src/Github/Exception/LatestReleaseNotFound.php b/src/Github/Exception/LatestReleaseNotFound.php index aa5f726f3..325b5cce8 100644 --- a/src/Github/Exception/LatestReleaseNotFound.php +++ b/src/Github/Exception/LatestReleaseNotFound.php @@ -13,6 +13,7 @@ namespace App\Github\Exception; +use App\Domain\Value\Branch; use App\Domain\Value\Repository; /** @@ -31,4 +32,17 @@ public static function forRepository(Repository $repository, ?\Throwable $previo $previous ); } + + public static function forRepositoryAndBranch(Repository $repository, Branch $branch, ?\Throwable $previous = null): self + { + return new self( + sprintf( + 'Could not find latest Release for the branch "%s" of "%s".', + $branch->name(), + $repository->toString() + ), + 0, + $previous + ); + } } diff --git a/templates/releases/overview.html.twig b/templates/releases/overview.html.twig index 97d26e5ec..e770df08d 100644 --- a/templates/releases/overview.html.twig +++ b/templates/releases/overview.html.twig @@ -11,6 +11,7 @@ Project + Branch Current Next Stability @@ -25,10 +26,13 @@ {% if release.isNeeded %} - + {{ release.project.title }} + + {{ release.branch.name }} + {{ release.currentTag.toString }} diff --git a/templates/releases/project.html.twig b/templates/releases/project.html.twig index 54ddb2f7d..d01873541 100644 --- a/templates/releases/project.html.twig +++ b/templates/releases/project.html.twig @@ -3,7 +3,7 @@ {% extends 'nav_and_content.html.twig' %} {% block content_title %} - Next Release for {{ release.project.title }} + Next Release for {{ release.project.title }} {{ release.branch.name }} {% endblock %} {% block content %} diff --git a/tests/Action/DetermineNextReleaseVersionTest.php b/tests/Action/DetermineNextReleaseVersionTest.php index 249c1aa02..865135007 100644 --- a/tests/Action/DetermineNextReleaseVersionTest.php +++ b/tests/Action/DetermineNextReleaseVersionTest.php @@ -41,7 +41,7 @@ public function returnsCurrentIfNoPullRequestsAreProvider() /** * @test */ - public function returnsCurrentIfNoMinorOrPatchStabilityIsFound() + public function returnsCurrentIfNoMajorOrMinorOrPatchStabilityIsFound() { $tag = Tag::fromString('1.1.0'); @@ -77,6 +77,50 @@ public function determine(string $expected, string $current, array $pullRequests */ public function determineProvider(): \Generator { + yield [ + '2.0.0', + '1.1.0', + [ + self::createPullRequestWithStability(Stability::unknown()), + self::createPullRequestWithStability(Stability::major()), + self::createPullRequestWithStability(Stability::minor()), + self::createPullRequestWithStability(Stability::patch()), + ], + ]; + + yield [ + '2.0.0', + '2.0.0-alpha-1', + [ + self::createPullRequestWithStability(Stability::unknown()), + self::createPullRequestWithStability(Stability::major()), + self::createPullRequestWithStability(Stability::minor()), + self::createPullRequestWithStability(Stability::patch()), + ], + ]; + + yield [ + '2.0.0', + '2.0.0.alpha.1', + [ + self::createPullRequestWithStability(Stability::unknown()), + self::createPullRequestWithStability(Stability::major()), + self::createPullRequestWithStability(Stability::minor()), + self::createPullRequestWithStability(Stability::patch()), + ], + ]; + + yield [ + '2.0.0', + '1.1.0', + [ + self::createPullRequestWithStability(Stability::unknown()), + self::createPullRequestWithStability(Stability::major()), + self::createPullRequestWithStability(Stability::minor()), + self::createPullRequestWithStability(Stability::patch()), + ], + ]; + yield [ '1.2.0', '1.1.0', diff --git a/tests/Action/Exception/CannotDeterminNextReleaseTest.php b/tests/Action/Exception/CannotDeterminNextReleaseTest.php index 8a746de98..c88007565 100644 --- a/tests/Action/Exception/CannotDeterminNextReleaseTest.php +++ b/tests/Action/Exception/CannotDeterminNextReleaseTest.php @@ -43,8 +43,9 @@ public function forProject() $config[ProjectTest::DEFAULT_CONFIG_NAME], $package ); + $branch = $project->unstableBranch(); - $cannotDetermineNextRelease = CannotDetermineNextRelease::forProject($project); + $cannotDetermineNextRelease = CannotDetermineNextRelease::forBranch($project, $branch); self::assertInstanceOf( \RuntimeException::class, @@ -52,7 +53,8 @@ public function forProject() ); self::assertSame( sprintf( - 'Cannot determine next release for Project "%s".', + 'Cannot determine next release for branch "%s" of project "%s".', + $branch->name(), $project->name() ), $cannotDetermineNextRelease->getMessage() diff --git a/tests/Action/Exception/NoPullRequestMergedSinceLastReleaseTest.php b/tests/Action/Exception/NoPullRequestMergedSinceLastReleaseTest.php index 843bf0512..201f6db5e 100644 --- a/tests/Action/Exception/NoPullRequestMergedSinceLastReleaseTest.php +++ b/tests/Action/Exception/NoPullRequestMergedSinceLastReleaseTest.php @@ -43,11 +43,13 @@ public function forProject() $config[ProjectTest::DEFAULT_CONFIG_NAME], $package ); + $branch = $project->unstableBranch(); $lastRelease = new \DateTimeImmutable($datetime = '2020-01-01 10:00:01'); - $cannotDetermineNextRelease = NoPullRequestsMergedSinceLastRelease::forProject( + $cannotDetermineNextRelease = NoPullRequestsMergedSinceLastRelease::forBranch( $project, + $branch, $lastRelease ); @@ -57,8 +59,9 @@ public function forProject() ); self::assertSame( sprintf( - 'No pull requests merged since last release "%s" for Project "%s".', + 'No pull requests merged since last release "%s" for branch "%s" of project "%s".', $datetime, + $branch->name(), $project->name() ), $cannotDetermineNextRelease->getMessage() diff --git a/tests/Config/Exception/UnknownBranchTest.php b/tests/Config/Exception/UnknownBranchTest.php new file mode 100644 index 000000000..676bc23b5 --- /dev/null +++ b/tests/Config/Exception/UnknownBranchTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Tests\Config\Exception; + +use App\Config\Exception\UnknownBranch; +use App\Domain\Value\Project; +use App\Tests\Domain\Value\ProjectTest; +use Ergebnis\Test\Util\Helper; +use Packagist\Api\Result\Package; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Yaml\Yaml; + +final class UnknownBranchTest extends TestCase +{ + use Helper; + + /** + * @test + */ + public function forName() + { + $package = new Package(); + $package->fromArray([ + 'name' => $packageName = 'sonata-project/admin-bundle', + 'repository' => 'https://github.com/sonata-project/SonataAdminBundle', + ]); + + $config = Yaml::parse(ProjectTest::DEFAULT_CONFIG); + + $project = Project::fromValues( + ProjectTest::DEFAULT_CONFIG_NAME, + $config[ProjectTest::DEFAULT_CONFIG_NAME], + $package + ); + + $name = self::faker()->word; + + $unknownBranch = UnknownBranch::forName($project, $name); + + self::assertInstanceOf( + \InvalidArgumentException::class, + $unknownBranch + ); + self::assertSame( + sprintf( + 'Could not find branch with name "%s" for project "%s".', + $name, + $project->name() + ), + $unknownBranch->getMessage() + ); + } +} diff --git a/tests/Config/Exception/UnknownProjectTest.php b/tests/Config/Exception/UnknownProjectTest.php index 93094177a..ba2163427 100644 --- a/tests/Config/Exception/UnknownProjectTest.php +++ b/tests/Config/Exception/UnknownProjectTest.php @@ -36,7 +36,7 @@ public function forName() ); self::assertSame( sprintf( - 'Could not find Project with name "%s".', + 'Could not find project with name "%s".', $name ), $unknownProject->getMessage()