diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9d7c20..5a7d5d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed -- Nothing. +- [#47](https://github.com/laminas/automatic-releases/pull/47) fixes `CHANGELOG.md` update operations to avoid preventable failures during the release process. ## 1.2.1 - 2020-08-12 diff --git a/bin/console.php b/bin/console.php index 836bcdf9..e838231e 100755 --- a/bin/console.php +++ b/bin/console.php @@ -13,6 +13,7 @@ use Laminas\AutomaticReleases\Application\Command\ReleaseCommand; use Laminas\AutomaticReleases\Application\Command\SwitchDefaultBranchToNextMinor; use Laminas\AutomaticReleases\Changelog\BumpAndCommitChangelogVersionViaKeepAChangelog; +use Laminas\AutomaticReleases\Changelog\ChangelogExistsViaConsole; use Laminas\AutomaticReleases\Changelog\CommitReleaseChangelogViaKeepAChangelog; use Laminas\AutomaticReleases\Environment\EnvironmentVariables; use Laminas\AutomaticReleases\Git\CheckoutBranchViaConsole; @@ -69,16 +70,24 @@ static function (int $errorCode, string $message = '', string $file = '', int $l $httpClient, $githubToken )); + $changelogExists = new ChangelogExistsViaConsole(); $checkoutBranch = new CheckoutBranchViaConsole(); $commit = new CommitFileViaConsole(); $push = new PushViaConsole(); - $commitChangelog = new CommitReleaseChangelogViaKeepAChangelog(new SystemClock(), $commit, $push, $logger); + $commitChangelog = new CommitReleaseChangelogViaKeepAChangelog( + new SystemClock(), + $changelogExists, + $checkoutBranch, + $commit, + $push, + $logger + ); $createCommitText = new CreateReleaseTextThroughChangelog(JwageGenerateChangelog::create( $makeRequests, $httpClient )); $createReleaseText = new ConcatenateMultipleReleaseTexts([ - new CreateReleaseTextViaKeepAChangelog(), + new CreateReleaseTextViaKeepAChangelog($changelogExists), $createCommitText, ]); $createRelease = new CreateReleaseThroughApiCall( @@ -87,6 +96,7 @@ static function (int $errorCode, string $message = '', string $file = '', int $l $githubToken ); $bumpChangelogVersion = new BumpAndCommitChangelogVersionViaKeepAChangelog( + $changelogExists, $checkoutBranch, $commit, $push, diff --git a/src/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelog.php b/src/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelog.php index 550af625..ca71ffbf 100644 --- a/src/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelog.php +++ b/src/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelog.php @@ -13,7 +13,6 @@ use Psr\Log\LoggerInterface; use Webmozart\Assert\Assert; -use function file_exists; use function sprintf; class BumpAndCommitChangelogVersionViaKeepAChangelog implements BumpAndCommitChangelogVersion @@ -26,21 +25,24 @@ class BumpAndCommitChangelogVersionViaKeepAChangelog implements BumpAndCommitCha Updates the %s file to add a changelog entry for a new %s version. COMMIT; + private ChangelogExists $changelogExists; private CheckoutBranch $checkoutBranch; private CommitFile $commitFile; private Push $push; private LoggerInterface $logger; public function __construct( + ChangelogExists $changelogExists, CheckoutBranch $checkoutBranch, CommitFile $commitFile, Push $push, LoggerInterface $logger ) { - $this->checkoutBranch = $checkoutBranch; - $this->commitFile = $commitFile; - $this->push = $push; - $this->logger = $logger; + $this->changelogExists = $changelogExists; + $this->checkoutBranch = $checkoutBranch; + $this->commitFile = $commitFile; + $this->push = $push; + $this->logger = $logger; } public function __invoke( @@ -49,16 +51,16 @@ public function __invoke( SemVerVersion $version, BranchName $sourceBranch ): void { - ($this->checkoutBranch)($repositoryDirectory, $sourceBranch); - - $changelogFile = sprintf('%s/%s', $repositoryDirectory, self::CHANGELOG_FILE); - if (! file_exists($changelogFile)) { + if (! ($this->changelogExists)($sourceBranch, $repositoryDirectory)) { // No changelog $this->logger->info('BumpAndCommitChangelog: No CHANGELOG.md file detected'); return; } + ($this->checkoutBranch)($repositoryDirectory, $sourceBranch); + + $changelogFile = sprintf('%s/%s', $repositoryDirectory, self::CHANGELOG_FILE); $versionString = $version->fullReleaseName(); $bumper = new ChangelogBump($changelogFile); $newVersion = $bumper->$bumpType($versionString); diff --git a/src/Changelog/ChangelogExists.php b/src/Changelog/ChangelogExists.php new file mode 100644 index 00000000..b511675b --- /dev/null +++ b/src/Changelog/ChangelogExists.php @@ -0,0 +1,18 @@ +name() . ':CHANGELOG.md'], $repositoryDirectory); + $process->run(); + + return $process->isSuccessful(); + } +} diff --git a/src/Changelog/CommitReleaseChangelogViaKeepAChangelog.php b/src/Changelog/CommitReleaseChangelogViaKeepAChangelog.php index 73829e42..5bcd8b20 100644 --- a/src/Changelog/CommitReleaseChangelogViaKeepAChangelog.php +++ b/src/Changelog/CommitReleaseChangelogViaKeepAChangelog.php @@ -4,6 +4,7 @@ namespace Laminas\AutomaticReleases\Changelog; +use Laminas\AutomaticReleases\Git\CheckoutBranch; use Laminas\AutomaticReleases\Git\CommitFile; use Laminas\AutomaticReleases\Git\Push; use Laminas\AutomaticReleases\Git\Value\BranchName; @@ -17,7 +18,6 @@ use Symfony\Component\Console\Output\NullOutput; use Webmozart\Assert\Assert; -use function file_exists; use function sprintf; final class CommitReleaseChangelogViaKeepAChangelog implements CommitReleaseChangelog @@ -31,20 +31,26 @@ final class CommitReleaseChangelogViaKeepAChangelog implements CommitReleaseChan COMMIT; private Clock $clock; + private ChangelogExists $changelogExists; + private CheckoutBranch $checkoutBranch; private CommitFile $commitFile; private Push $push; private LoggerInterface $logger; public function __construct( Clock $clock, + ChangelogExists $changelogExists, + CheckoutBranch $checkoutBranch, CommitFile $commitFile, Push $push, LoggerInterface $logger ) { - $this->clock = $clock; - $this->commitFile = $commitFile; - $this->push = $push; - $this->logger = $logger; + $this->clock = $clock; + $this->changelogExists = $changelogExists; + $this->checkoutBranch = $checkoutBranch; + $this->commitFile = $commitFile; + $this->push = $push; + $this->logger = $logger; } /** @@ -55,16 +61,18 @@ public function __invoke( SemVerVersion $version, BranchName $sourceBranch ): void { - $changelogFile = sprintf('%s/%s', $repositoryDirectory, self::CHANGELOG_FILE); - if (! file_exists($changelogFile)) { + if (! ($this->changelogExists)($sourceBranch, $repositoryDirectory)) { // No changelog $this->logger->info('No CHANGELOG.md file detected'); return; } + $changelogFile = sprintf('%s/%s', $repositoryDirectory, self::CHANGELOG_FILE); $versionString = $version->fullReleaseName(); + ($this->checkoutBranch)($repositoryDirectory, $sourceBranch); + if (! $this->updateChangelog($changelogFile, $versionString)) { // Failure to update; nothing to commit return; diff --git a/src/Github/CreateReleaseTextViaKeepAChangelog.php b/src/Github/CreateReleaseTextViaKeepAChangelog.php index cc7c42bd..43672ad8 100644 --- a/src/Github/CreateReleaseTextViaKeepAChangelog.php +++ b/src/Github/CreateReleaseTextViaKeepAChangelog.php @@ -5,19 +5,25 @@ namespace Laminas\AutomaticReleases\Github; use InvalidArgumentException; +use Laminas\AutomaticReleases\Changelog\ChangelogExists; use Laminas\AutomaticReleases\Git\Value\BranchName; use Laminas\AutomaticReleases\Git\Value\SemVerVersion; use Laminas\AutomaticReleases\Github\Api\GraphQL\Query\GetMilestoneChangelog\Response\Milestone; use Laminas\AutomaticReleases\Github\Value\RepositoryName; use Phly\KeepAChangelog\Common\ChangelogParser; use Phly\KeepAChangelog\Exception\ExceptionInterface; +use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; -use function file_exists; -use function file_get_contents; - class CreateReleaseTextViaKeepAChangelog implements CreateReleaseText { + private ChangelogExists $changelogExists; + + public function __construct(ChangelogExists $changelogExists) + { + $this->changelogExists = $changelogExists; + } + public function __invoke( Milestone $milestone, RepositoryName $repositoryName, @@ -27,7 +33,7 @@ public function __invoke( ): string { $changelog = (new ChangelogParser()) ->findChangelogForVersion( - file_get_contents($repositoryDirectory . '/CHANGELOG.md'), + $this->fetchChangelogContentsFromBranch($sourceBranch, $repositoryDirectory), $semVerVersion->fullReleaseName() ); @@ -43,15 +49,14 @@ public function canCreateReleaseText( BranchName $sourceBranch, string $repositoryDirectory ): bool { - $changelogFile = $repositoryDirectory . '/CHANGELOG.md'; - if (! file_exists($changelogFile)) { + if (! ($this->changelogExists)($sourceBranch, $repositoryDirectory)) { return false; } try { $changelog = (new ChangelogParser()) ->findChangelogForVersion( - file_get_contents($changelogFile), + $this->fetchChangelogContentsFromBranch($sourceBranch, $repositoryDirectory), $semVerVersion->fullReleaseName() ); @@ -62,4 +67,21 @@ public function canCreateReleaseText( return false; } } + + /** + * @psalm-param non-empty-string $repositoryDirectory + * @psalm-return non-empty-string + */ + private function fetchChangelogContentsFromBranch( + BranchName $sourceBranch, + string $repositoryDirectory + ): string { + $process = new Process(['git', 'show', $sourceBranch->name() . ':CHANGELOG.md'], $repositoryDirectory); + $process->mustRun(); + + $contents = $process->getOutput(); + Assert::notEmpty($contents); + + return $contents; + } } diff --git a/test/unit/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelogTest.php b/test/unit/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelogTest.php index 8a388e23..39e64182 100644 --- a/test/unit/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelogTest.php +++ b/test/unit/Changelog/BumpAndCommitChangelogVersionViaKeepAChangelogTest.php @@ -6,6 +6,8 @@ use Laminas\AutomaticReleases\Changelog\BumpAndCommitChangelogVersion; use Laminas\AutomaticReleases\Changelog\BumpAndCommitChangelogVersionViaKeepAChangelog; +use Laminas\AutomaticReleases\Changelog\ChangelogExists; +use Laminas\AutomaticReleases\Changelog\ChangelogExistsViaConsole; use Laminas\AutomaticReleases\Git\CheckoutBranch; use Laminas\AutomaticReleases\Git\CommitFile; use Laminas\AutomaticReleases\Git\Push; @@ -44,6 +46,7 @@ protected function setUp(): void $this->push = $this->createMock(Push::class); $this->logger = $this->createMock(LoggerInterface::class); $this->bumpAndCommitChangelog = new BumpAndCommitChangelogVersionViaKeepAChangelog( + new ChangelogExistsViaConsole(), $this->checkoutBranch, $this->commitFile, $this->push, @@ -58,12 +61,8 @@ public function testReturnsEarlyWhenNoChangelogFilePresent(): void $version = SemVerVersion::fromMilestoneName('1.0.1'); $this->checkoutBranch - ->expects($this->once()) - ->method('__invoke') - ->with( - $this->equalTo($repoDir), - $sourceBranch - ); + ->expects($this->never()) + ->method('__invoke'); $this->logger ->expects($this->once()) @@ -118,6 +117,13 @@ public function testAddsNewReleaseVersionUsingBumpTypeToChangelogFileAndCommitsA Assert::stringNotEmpty($repoDir); + $changelogExists = $this->createMock(ChangelogExists::class); + $changelogExists + ->expects($this->once()) + ->method('__invoke') + ->with($sourceBranch, $repoDir) + ->willReturn(true); + $this->logger ->expects($this->once()) ->method('info') @@ -154,8 +160,16 @@ public function testAddsNewReleaseVersionUsingBumpTypeToChangelogFileAndCommitsA ->with( ); + $bumpAndCommitChangelog = new BumpAndCommitChangelogVersionViaKeepAChangelog( + $changelogExists, + $this->checkoutBranch, + $this->commitFile, + $this->push, + $this->logger + ); + $this->assertNull( - ($this->bumpAndCommitChangelog)( + $bumpAndCommitChangelog( $bumpType, $repoDir, $version, @@ -191,6 +205,13 @@ private function createMockChangelog(): string file_put_contents($changelogFile, self::CHANGELOG_STUB); + (new Process(['git', 'init', '.'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.email', 'me@example.com'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.name', 'Just Me'], $repo))->mustRun(); + (new Process(['git', 'add', '.'], $repo))->mustRun(); + (new Process(['git', 'commit', '-m', 'Initial import'], $repo))->mustRun(); + (new Process(['git', 'switch', '-c', '1.0.x'], $repo))->mustRun(); + return $changelogFile; } diff --git a/test/unit/Changelog/ChangelogExistsViaConsoleTest.php b/test/unit/Changelog/ChangelogExistsViaConsoleTest.php new file mode 100644 index 00000000..756e6359 --- /dev/null +++ b/test/unit/Changelog/ChangelogExistsViaConsoleTest.php @@ -0,0 +1,93 @@ +createMockRepositoryWithChangelog() + ) + ); + } + + public function testReturnsTrueWhenChangelogIsPresentInBranch(): void + { + self::assertTrue( + (new ChangelogExistsViaConsole())( + BranchName::fromName('1.0.x'), + $this->createMockRepositoryWithChangelog() + ) + ); + } + + /** + * @psalm-return non-empty-string + */ + private function createMockRepositoryWithChangelog(): string + { + $repo = tempnam(sys_get_temp_dir(), 'ChangelogExists'); + Assert::notEmpty($repo); + unlink($repo); + + (new Process(['mkdir', '-p', $repo]))->mustRun(); + + file_put_contents( + sprintf('%s/%s', $repo, 'CHANGELOG.md'), + <<< 'CHANGELOG' + # Changelog + + All notable changes to this project will be documented in this file, in reverse chronological order by release. + + ## 1.0.0 - %s + + ### Added + + - Everything. + + ### Changed + + - Nothing. + + ### Deprecated + + - Nothing. + + ### Removed + + - Nothing. + + ### Fixed + + - Nothing. + + CHANGELOG + ); + + (new Process(['git', 'init', '.'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.email', 'me@example.com'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.name', 'Just Me'], $repo))->mustRun(); + (new Process(['git', 'add', '.'], $repo))->mustRun(); + (new Process(['git', 'commit', '-m', 'Initial import'], $repo))->mustRun(); + (new Process(['git', 'switch', '-c', '1.0.x'], $repo))->mustRun(); + + return $repo; + } +} diff --git a/test/unit/Changelog/ReleaseChangelogViaKeepAChangelogTest.php b/test/unit/Changelog/ReleaseChangelogViaKeepAChangelogTest.php index 458e5ee9..9b967224 100644 --- a/test/unit/Changelog/ReleaseChangelogViaKeepAChangelogTest.php +++ b/test/unit/Changelog/ReleaseChangelogViaKeepAChangelogTest.php @@ -5,7 +5,9 @@ namespace Laminas\AutomaticReleases\Test\Unit\Changelog; use DateTimeImmutable; +use Laminas\AutomaticReleases\Changelog\ChangelogExistsViaConsole; use Laminas\AutomaticReleases\Changelog\CommitReleaseChangelogViaKeepAChangelog; +use Laminas\AutomaticReleases\Git\CheckoutBranch; use Laminas\AutomaticReleases\Git\CommitFile; use Laminas\AutomaticReleases\Git\Push; use Laminas\AutomaticReleases\Git\Value\BranchName; @@ -28,6 +30,9 @@ class ReleaseChangelogViaKeepAChangelogTest extends TestCase { private Clock $clock; + /** @var CheckoutBranch&MockObject */ + private CheckoutBranch $checkoutBranch; + /** @var CommitFile&MockObject */ private CommitFile $commitFile; @@ -41,13 +46,16 @@ class ReleaseChangelogViaKeepAChangelogTest extends TestCase protected function setUp(): void { - $this->clock = new FrozenClock(new DateTimeImmutable('2020-08-05T00:00:01Z')); - $this->commitFile = $this->createMock(CommitFile::class); - $this->push = $this->createMock(Push::class); - $this->logger = $this->createMock(LoggerInterface::class); + $this->clock = new FrozenClock(new DateTimeImmutable('2020-08-05T00:00:01Z')); + $this->checkoutBranch = $this->createMock(CheckoutBranch::class); + $this->commitFile = $this->createMock(CommitFile::class); + $this->push = $this->createMock(Push::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->releaseChangelog = new CommitReleaseChangelogViaKeepAChangelog( $this->clock, + new ChangelogExistsViaConsole(), + $this->checkoutBranch, $this->commitFile, $this->push, $this->logger @@ -56,6 +64,10 @@ protected function setUp(): void public function testNoOpWhenChangelogFileDoesNotExist(): void { + $this->checkoutBranch + ->expects($this->never()) + ->method('__invoke'); + $this->logger ->expects($this->once()) ->method('info') @@ -66,14 +78,22 @@ public function testNoOpWhenChangelogFileDoesNotExist(): void self::assertNull( $this->releaseChangelog->__invoke( __DIR__, - SemVerVersion::fromMilestoneName('1.0.0'), - BranchName::fromName('1.0.x') + SemVerVersion::fromMilestoneName('0.99.99'), + BranchName::fromName('0.99.x') ) ); } public function testNoOpWhenUnableToFindMatchingChangelogEntry(): void { + $repo = $this->createMockRepositoryWithChangelog(self::INVALID_CHANGELOG); + $branch = BranchName::fromName('1.0.x'); + + $this->checkoutBranch + ->expects($this->once()) + ->method('__invoke') + ->with($repo, $branch); + $this ->logger ->expects($this->once()) @@ -84,15 +104,23 @@ public function testNoOpWhenUnableToFindMatchingChangelogEntry(): void self::assertNull( $this->releaseChangelog->__invoke( - $this->createMockRepositoryWithChangelog(self::INVALID_CHANGELOG), + $repo, SemVerVersion::fromMilestoneName('1.0.0'), - BranchName::fromName('1.0.x') + $branch ) ); } public function testNoOpWhenFailedToSetReleaseDateInChangelogEntry(): void { + $repo = $this->createMockRepositoryWithChangelog(self::RELEASED_CHANGELOG); + $branch = BranchName::fromName('1.0.x'); + + $this->checkoutBranch + ->expects($this->once()) + ->method('__invoke') + ->with($repo, $branch); + $this ->logger ->expects($this->once()) @@ -103,9 +131,9 @@ public function testNoOpWhenFailedToSetReleaseDateInChangelogEntry(): void self::assertNull( $this->releaseChangelog->__invoke( - $this->createMockRepositoryWithChangelog(self::RELEASED_CHANGELOG), + $repo, SemVerVersion::fromMilestoneName('1.0.0'), - BranchName::fromName('1.0.x') + $branch ) ); } @@ -117,6 +145,11 @@ public function testWritesCommitsAndPushesChangelogWhenFoundAndReadyToRelease(): $repositoryPath = $this->createMockRepositoryWithChangelog($existingChangelog); $sourceBranch = BranchName::fromName('1.0.x'); + $this->checkoutBranch + ->expects($this->once()) + ->method('__invoke') + ->with($repositoryPath, $sourceBranch); + $this ->logger ->expects($this->once()) @@ -170,6 +203,13 @@ private function createMockRepositoryWithChangelog( $template ); + (new Process(['git', 'init', '.'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.email', 'me@example.com'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.name', 'Just Me'], $repo))->mustRun(); + (new Process(['git', 'add', '.'], $repo))->mustRun(); + (new Process(['git', 'commit', '-m', 'Initial import'], $repo))->mustRun(); + (new Process(['git', 'switch', '-c', '1.0.x'], $repo))->mustRun(); + return $repo; } diff --git a/test/unit/Github/CreateReleaseTextViaKeepAChangelogTest.php b/test/unit/Github/CreateReleaseTextViaKeepAChangelogTest.php index e6f7c331..f1f4abfa 100644 --- a/test/unit/Github/CreateReleaseTextViaKeepAChangelogTest.php +++ b/test/unit/Github/CreateReleaseTextViaKeepAChangelogTest.php @@ -4,6 +4,7 @@ namespace Laminas\AutomaticReleases\Test\Unit\Github; +use Laminas\AutomaticReleases\Changelog\ChangelogExistsViaConsole; use Laminas\AutomaticReleases\Git\Value\BranchName; use Laminas\AutomaticReleases\Git\Value\SemVerVersion; use Laminas\AutomaticReleases\Github\Api\GraphQL\Query\GetMilestoneChangelog\Response\Milestone; @@ -30,7 +31,7 @@ public function testReportsCannotCreateReleaseTextIfChangelogFileIsMissing(): vo ); self::assertFalse( - (new CreateReleaseTextViaKeepAChangelog()) + (new CreateReleaseTextViaKeepAChangelog(new ChangelogExistsViaConsole())) ->canCreateReleaseText( $this->createMockMilestone(), RepositoryName::fromFullName('example/repo'), @@ -49,7 +50,7 @@ public function testReportsCannotCreateReleaseTextIfChangelogFileDoesNotContainV ); self::assertFalse( - (new CreateReleaseTextViaKeepAChangelog()) + (new CreateReleaseTextViaKeepAChangelog(new ChangelogExistsViaConsole())) ->canCreateReleaseText( $this->createMockMilestone(), RepositoryName::fromFullName('example/repo'), @@ -69,7 +70,7 @@ public function testReportsCanCreateReleaseWhenChangelogWithVersionExists(): voi ); self::assertTrue( - (new CreateReleaseTextViaKeepAChangelog()) + (new CreateReleaseTextViaKeepAChangelog(new ChangelogExistsViaConsole())) ->canCreateReleaseText( $this->createMockMilestone(), RepositoryName::fromFullName('example/repo'), @@ -113,7 +114,7 @@ public function testExtractsReleaseTextViaChangelogFile(): void self::assertSame( $expected, - (new CreateReleaseTextViaKeepAChangelog()) + (new CreateReleaseTextViaKeepAChangelog(new ChangelogExistsViaConsole())) ->__invoke( $this->createMockMilestone(), RepositoryName::fromFullName('example/repo'), @@ -159,6 +160,13 @@ private function createMockRepositoryWithChangelog( $template ); + (new Process(['git', 'init', '.'], $repo))->mustRun(); + (new Process(['git', 'add', '.'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.email', 'me@example.com'], $repo))->mustRun(); + (new Process(['git', 'config', 'user.name', 'Just Me'], $repo))->mustRun(); + (new Process(['git', 'commit', '-m', 'Initial import'], $repo))->mustRun(); + (new Process(['git', 'switch', '-c', '1.0.x'], $repo))->mustRun(); + return $repo; }