diff --git a/src/Command/Push/PushArtifactCommand.php b/src/Command/Push/PushArtifactCommand.php index 0a1271f94..21eb95c6d 100644 --- a/src/Command/Push/PushArtifactCommand.php +++ b/src/Command/Push/PushArtifactCommand.php @@ -47,7 +47,10 @@ protected function configure(): void { $this->setDescription('Build and push a code artifact to a Cloud Platform environment') ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be pushed') ->addOption('no-sanitize', NULL, InputOption::VALUE_NONE, 'Do not sanitize the build artifact') - ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Do not push changes to Acquia Cloud') + ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Deprecated: Use no-push instead') + ->addOption('no-push', NULL, InputOption::VALUE_NONE, 'Do not push changes to Acquia Cloud') + ->addOption('no-commit', NULL, InputOption::VALUE_NONE, 'Do not commit changes. Implies no-push') + ->addOption('no-clone', NULL, InputOption::VALUE_NONE, 'Do not clone repository. Implies no-commit and no-push') ->addOption('destination-git-urls', 'u', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The URL(s) of your git repository to which the artifact branch will be pushed') ->addOption('destination-git-branch', 'b', InputOption::VALUE_REQUIRED, 'The destination branch to push the artifact to') ->addOption('destination-git-tag', 't', InputOption::VALUE_REQUIRED, 'The destination tag to push the artifact to. Using this option requires also using the --source-git-tag option') @@ -61,8 +64,18 @@ protected function configure(): void { ->addUsage('--destination-git-urls=example@svn-1.prod.hosting.acquia.com:example.git --destination-git-branch=main-build'); } + protected function commandRequiresAuthentication(): bool { + return FALSE; + } + protected function execute(InputInterface $input, OutputInterface $output): int { $this->setDirAndRequireProjectCwd($input); + if ($input->getOption('no-clone')) { + $input->setOption('no-commit', TRUE); + } + if ($input->getOption('no-commit')) { + $input->setOption('no-push', TRUE); + } $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); $this->composerJsonPath = Path::join($this->dir, 'composer.json'); $this->docrootPath = Path::join($this->dir, 'docroot'); @@ -74,27 +87,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new AcquiaCliException('Pushing code was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.'); } $this->checklist = new Checklist($output); - $applicationUuid = $this->determineCloudApplication(); - $destinationGitUrls = $this->determineDestinationGitUrls($applicationUuid); - $destinationGitRef = $this->determineDestinationGitRef(); - $sourceGitBranch = $this->determineSourceGitRef(); - - $destinationGitUrlsString = implode(',', $destinationGitUrls); - $refType = $this->input->getOption('destination-git-tag') ? 'tag' : 'branch'; - $this->io->note([ - "Acquia CLI will:", - "- git clone $sourceGitBranch from $destinationGitUrls[0]", - "- Compile the contents of $this->dir into an artifact in a temporary directory", - "- Copy the artifact files into the checked out copy of $sourceGitBranch", - "- Commit changes and push the $destinationGitRef $refType to the following git remote(s):", - " $destinationGitUrlsString", - ]); - $outputCallback = $this->getOutputCallback($output, $this->checklist); - $this->checklist->addItem('Preparing artifact directory'); - $this->cloneSourceBranch($outputCallback, $artifactDir, $destinationGitUrls[0], $sourceGitBranch); - $this->checklist->completePreviousItem(); + $destinationGitUrls = []; + $destinationGitRef = ''; + if (!$input->getOption('no-clone')) { + $applicationUuid = $this->determineCloudApplication(); + $destinationGitUrls = $this->determineDestinationGitUrls($applicationUuid); + $destinationGitRef = $this->determineDestinationGitRef(); + $sourceGitBranch = $this->determineSourceGitRef(); + $destinationGitUrlsString = implode(',', $destinationGitUrls); + $refType = $this->input->getOption('destination-git-tag') ? 'tag' : 'branch'; + $this->io->note([ + "Acquia CLI will:", + "- git clone $sourceGitBranch from $destinationGitUrls[0]", + "- Compile the contents of $this->dir into an artifact in a temporary directory", + "- Copy the artifact files into the checked out copy of $sourceGitBranch", + "- Commit changes and push the $destinationGitRef $refType to the following git remote(s):", + " $destinationGitUrlsString", + ]); + + $this->checklist->addItem('Preparing artifact directory'); + $this->cloneSourceBranch($outputCallback, $artifactDir, $destinationGitUrls[0], $sourceGitBranch); + $this->checklist->completePreviousItem(); + } $this->checklist->addItem('Generating build artifact'); $this->buildArtifact($outputCallback, $artifactDir); @@ -106,11 +122,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->checklist->completePreviousItem(); } - $this->checklist->addItem("Committing changes (commit hash: $commitHash)"); - $this->commit($outputCallback, $artifactDir, $commitHash); - $this->checklist->completePreviousItem(); + if (!$input->getOption('no-commit')) { + $this->checklist->addItem("Committing changes (commit hash: $commitHash)"); + $this->commit($outputCallback, $artifactDir, $commitHash); + $this->checklist->completePreviousItem(); + } - if (!$input->getOption('dry-run')) { + if (!$input->getOption('dry-run') && !$input->getOption('no-push')) { if ($tagName = $input->getOption('destination-git-tag')) { $this->checklist->addItem("Creating $tagName tag."); $this->createTag($tagName, $outputCallback, $artifactDir); @@ -125,13 +143,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->checklist->completePreviousItem(); } else { - $this->logger->warning("The --dry-run option prevented changes from being pushed to Acquia Cloud. The artifact has been built at $artifactDir"); + $this->logger->warning("The --dry-run (deprecated) or --no-push option prevented changes from being pushed to Acquia Cloud. The artifact has been built at $artifactDir"); } return Command::SUCCESS; } - private function determineDestinationGitUrls(?string $applicationUuid): mixed { + /** + * @return string[] + */ + private function determineDestinationGitUrls(?string $applicationUuid): array { if ($this->input->getOption('destination-git-urls')) { return $this->input->getOption('destination-git-urls'); } @@ -305,8 +326,6 @@ private function generateCommitMessage(string $commitHash): array|string { /** * Push the artifact. - * - * @param array $vcsUrls */ private function pushArtifact(Closure $outputCallback, string $artifactDir, array $vcsUrls, string $destGitBranch): void { $this->localMachineHelper->checkRequiredBinariesExist(['git']); @@ -383,7 +402,7 @@ private function validateSourceCode(): void { } } - private function determineSourceGitRef(): mixed { + private function determineSourceGitRef(): string { if ($this->input->getOption('source-git-tag')) { return $this->input->getOption('source-git-tag'); } @@ -398,7 +417,7 @@ private function determineSourceGitRef(): mixed { return $this->destinationGitRef; } - private function determineDestinationGitRef(): mixed { + private function determineDestinationGitRef(): string { if ($this->input->getOption('destination-git-tag')) { $this->destinationGitRef = $this->input->getOption('destination-git-tag'); return $this->destinationGitRef; diff --git a/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php b/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php index b13a641a4..8e367a7c9 100644 --- a/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushArtifactCommandTest.php @@ -22,16 +22,17 @@ protected function createCommand(): Command { return $this->injectCommand(PushArtifactCommand::class); } + public function testNoAuthenticationRequired(): void { + $help = $this->command->getHelp(); + $this->assertStringNotContainsString('This command requires authentication', $help); + } + public function testPushArtifact(): void { - touch(Path::join($this->projectDir, 'composer.json')); - mkdir(Path::join($this->projectDir, 'docroot')); - $this->mockRequest('getApplications'); - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, $selectedEnvironment->vcs->path, [$selectedEnvironment->vcs->url]); + $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url]); $inputs = [ // Would you like Acquia CLI to search for a Cloud application that matches your local git config? 'n', @@ -50,19 +51,30 @@ public function testPushArtifact(): void { $this->assertStringContainsString('[0] Sample application 1', $output); $this->assertStringContainsString('Choose a Cloud Platform environment', $output); $this->assertStringContainsString('[0] Dev, dev (vcs: master)', $output); + $this->assertStringContainsString('Acquia CLI will:', $output); + $this->assertStringContainsString('- git clone master from site@svn-3.hosted.acquia-sites.com:site.git', $output); + $this->assertStringContainsString('- Compile the contents of vfs://root/project into an artifact', $output); + $this->assertStringContainsString('- Copy the artifact files into the checked out copy of master', $output); + $this->assertStringContainsString('- Commit changes and push the master branch', $output); + $this->assertStringContainsString('Removing', $output); + $this->assertStringContainsString('Initializing Git', $output); + $this->assertStringContainsString('Global .gitignore file', $output); + $this->assertStringContainsString('Removing vendor', $output); + $this->assertStringContainsString('Mirroring source', $output); + $this->assertStringContainsString('Installing Composer', $output); + $this->assertStringContainsString('Finding Drupal', $output); + $this->assertStringContainsString('Removing sensitive', $output); + $this->assertStringContainsString('Adding and committing', $output); $this->assertStringContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); } public function testPushTagArtifact(): void { - touch(Path::join($this->projectDir, 'composer.json')); - mkdir(Path::join($this->projectDir, 'docroot')); - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $environmentsResponse = $this->mockEnvironmentsRequest($applicationsResponse); - $selectedEnvironment = $environmentsResponse->_embedded->items[0]; + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); $localMachineHelper = $this->mockLocalMachineHelper(); - $this->setUpPushArtifact($localMachineHelper, '1.2.0', [$selectedEnvironment->vcs->url]); $gitTag = '1.2.0-build'; + $this->setUpPushArtifact($localMachineHelper, '1.2.0', [$environments[0]->vcs->url], $gitTag); $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); $this->mockGitTag($localMachineHelper, $gitTag, $artifactDir); $inputs = [ @@ -83,15 +95,13 @@ public function testPushTagArtifact(): void { $this->assertStringContainsString('Select a Cloud Platform application:', $output); $this->assertStringContainsString('[0] Sample application 1', $output); $this->assertStringContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + $this->assertStringContainsString('Commit changes and push the 1.2.0-build tag', $output); } public function testPushArtifactWithAcquiaCliFile(): void { - touch(Path::join($this->projectDir, 'composer.json')); - mkdir(Path::join($this->projectDir, 'docroot')); - $this->mockRequest('getApplications'); - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $this->mockEnvironmentsRequest($applicationsResponse); + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); $this->datastoreAcli->set('push.artifact.destination-git-urls', [ 'https://github.com/example1/cli.git', 'https://github.com/example2/cli.git', @@ -100,7 +110,7 @@ public function testPushArtifactWithAcquiaCliFile(): void { $this->setUpPushArtifact($localMachineHelper, 'master', $this->datastoreAcli->get('push.artifact.destination-git-urls')); $this->executeCommand([ '--destination-git-branch' => 'master', - ], []); + ]); $this->prophet->checkPredictions(); $output = $this->getDisplay(); @@ -109,12 +119,9 @@ public function testPushArtifactWithAcquiaCliFile(): void { } public function testPushArtifactWithArgs(): void { - touch(Path::join($this->projectDir, 'composer.json')); - mkdir(Path::join($this->projectDir, 'docroot')); - $this->mockRequest('getApplications'); - $applicationsResponse = $this->mockApplicationsRequest(); - $this->mockApplicationRequest(); - $this->mockEnvironmentsRequest($applicationsResponse); + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); $destinationGitUrls = [ 'https://github.com/example1/cli.git', 'https://github.com/example2/cli.git', @@ -124,7 +131,7 @@ public function testPushArtifactWithArgs(): void { $this->executeCommand([ '--destination-git-branch' => 'master', '--destination-git-urls' => $destinationGitUrls, - ], []); + ]); $this->prophet->checkPredictions(); $output = $this->getDisplay(); @@ -132,7 +139,81 @@ public function testPushArtifactWithArgs(): void { $this->assertStringContainsString('Pushing changes to Acquia Git (https://github.com/example2/cli.git)', $output); } - protected function setUpPushArtifact(mixed $localMachineHelper, mixed $vcsPath, mixed $vcsUrls): void { + public function testPushArtifactNoPush(): void { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url], 'master:master', TRUE, TRUE, FALSE); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand(['--no-push' => TRUE], $inputs); + $this->prophet->checkPredictions(); + $output = $this->getDisplay(); + + $this->assertStringContainsString('Initializing Git', $output); + $this->assertStringContainsString('Adding and committing changed files', $output); + $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + public function testPushArtifactNoCommit(): void { + $applications = $this->mockRequest('getApplications'); + $this->mockRequest('getApplicationByUuid', $applications[0]->uuid); + $environments = $this->mockRequest('getApplicationEnvironments', $applications[0]->uuid); + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, $environments[0]->vcs->path, [$environments[0]->vcs->url], 'master:master', TRUE, FALSE, FALSE); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand(['--no-commit' => TRUE], $inputs); + $this->prophet->checkPredictions(); + $output = $this->getDisplay(); + + $this->assertStringContainsString('Initializing Git', $output); + $this->assertStringNotContainsString('Adding and committing changed files', $output); + $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + public function testPushArtifactNoClone(): void { + $localMachineHelper = $this->mockLocalMachineHelper(); + $this->setUpPushArtifact($localMachineHelper, 'nothing', [], 'something', FALSE, FALSE, FALSE); + $inputs = [ + // Would you like Acquia CLI to search for a Cloud application that matches your local git config? + 'n', + // Select a Cloud Platform application: + 0, + // Would you like to link the project at ... ? + 'y', + // Choose an Acquia environment: + 0, + ]; + $this->executeCommand(['--no-clone' => TRUE], $inputs); + $this->prophet->checkPredictions(); + $output = $this->getDisplay(); + + $this->assertStringNotContainsString('Initializing Git', $output); + $this->assertStringNotContainsString('Adding and committing changed files', $output); + $this->assertStringNotContainsString('Pushing changes to Acquia Git (site@svn-3.hosted.acquia-sites.com:site.git)', $output); + } + + protected function setUpPushArtifact(ObjectProphecy $localMachineHelper, string $vcsPath, array $vcsUrls, string $destGitRef = 'master:master', bool $clone = TRUE, bool $commit = TRUE, bool $push = TRUE): void { + touch(Path::join($this->projectDir, 'composer.json')); + mkdir(Path::join($this->projectDir, 'docroot')); $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact'); $this->createMockGitConfigFile(); $finder = $this->mockFinder(); @@ -141,21 +222,28 @@ protected function setUpPushArtifact(mixed $localMachineHelper, mixed $vcsPath, $localMachineHelper->getFilesystem()->willReturn($fs)->shouldBeCalled(); $this->command->localMachineHelper = $localMachineHelper->reveal(); - $commitHash = 'abc123'; $this->mockExecuteGitStatus(FALSE, $localMachineHelper, $this->projectDir); + $commitHash = 'abc123'; $this->mockGetLocalCommitHash($localMachineHelper, $this->projectDir, $commitHash); - $this->mockCloneShallow($localMachineHelper, $vcsPath, $vcsUrls[0], $artifactDir); - $this->mockLocalGitConfig($localMachineHelper, $artifactDir); $this->mockComposerInstall($localMachineHelper, $artifactDir); $this->mockReadComposerJson($localMachineHelper, $artifactDir); - $this->mockGitAddCommit($localMachineHelper, $artifactDir, $commitHash); - $this->mockGitPush($vcsUrls, $localMachineHelper, $vcsPath, $artifactDir); + $localMachineHelper->checkRequiredBinariesExist(['git'])->shouldBeCalled(); + + if ($clone) { + $this->mockLocalGitConfig($localMachineHelper, $artifactDir); + $this->mockCloneShallow($localMachineHelper, $vcsPath, $vcsUrls[0], $artifactDir); + } + if ($commit) { + $this->mockGitAddCommit($localMachineHelper, $artifactDir, $commitHash); + } + if ($push) { + $this->mockGitPush($vcsUrls, $localMachineHelper, $artifactDir, $destGitRef); + } } - protected function mockCloneShallow(ObjectProphecy $localMachineHelper, mixed $vcsPath, mixed $vcsUrl, mixed $artifactDir): void { + protected function mockCloneShallow(ObjectProphecy $localMachineHelper, string $vcsPath, string $vcsUrl, mixed $artifactDir): void { $process = $this->prophet->prophesize(Process::class); $process->isSuccessful()->willReturn(TRUE)->shouldBeCalled(); - $localMachineHelper->checkRequiredBinariesExist(['git'])->shouldBeCalled(); $localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], Argument::type('callable'), NULL, TRUE) ->willReturn($process->reveal())->shouldBeCalled(); $localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], Argument::type('callable'), Argument::type('string'), TRUE) @@ -164,7 +252,7 @@ protected function mockCloneShallow(ObjectProphecy $localMachineHelper, mixed $v ->willReturn($process->reveal())->shouldBeCalled(); } - protected function mockLocalGitConfig(ObjectProphecy $localMachineHelper, mixed $artifactDir): void { + protected function mockLocalGitConfig(ObjectProphecy $localMachineHelper, string $artifactDir): void { $process = $this->prophet->prophesize(Process::class); $localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], Argument::type('callable'), $artifactDir, TRUE) ->willReturn($process->reveal())->shouldBeCalled(); @@ -215,15 +303,15 @@ protected function mockReadComposerJson(ObjectProphecy $localMachineHelper, stri ->willReturn($composerJson); } - protected function mockGitPush(mixed $gitUrls, ObjectProphecy $localMachineHelper, mixed $gitBranch, mixed $artifactDir): void { + protected function mockGitPush(array $gitUrls, ObjectProphecy $localMachineHelper, string $artifactDir, string $destGitRef): void { $process = $this->mockProcess(); foreach ($gitUrls as $gitUrl) { - $localMachineHelper->execute(Argument::containing($gitUrl), Argument::type('callable'), $artifactDir, TRUE) + $localMachineHelper->execute(['git', 'push', $gitUrl, $destGitRef], Argument::type('callable'), $artifactDir, TRUE) ->willReturn($process->reveal())->shouldBeCalled(); } } - protected function mockGitTag(ObjectProphecy $localMachineHelper, mixed $gitTag, mixed $artifactDir): void { + protected function mockGitTag(ObjectProphecy $localMachineHelper, string $gitTag, string $artifactDir): void { $process = $this->mockProcess(); $localMachineHelper->execute([ 'git',