From ebf20464409960ac20d53b3f539e202c7ecab937 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli <36352093+GuySartorelli@users.noreply.github.com> Date: Wed, 8 May 2024 18:21:58 +1200 Subject: [PATCH] MNT Remove redundant commands (#254) These commands are redundant - we use silverstripe/module-standardiser instead. --- README.md | 39 +--- src/Application.php | 6 - src/Commands/GitHub/SyncLabels.php | 226 -------------------- src/Commands/Module/AbstractSyncCommand.php | 163 -------------- src/Commands/Module/Sync/Metadata.php | 157 -------------- src/Utility/SupportedModuleLoader.php | 106 --------- tests/Commands/Module/Sync/MetadataTest.php | 131 ------------ tests/Utility/SupportedModuleLoaderTest.php | 101 --------- 8 files changed, 2 insertions(+), 927 deletions(-) delete mode 100644 src/Commands/GitHub/SyncLabels.php delete mode 100644 src/Commands/Module/AbstractSyncCommand.php delete mode 100644 src/Commands/Module/Sync/Metadata.php delete mode 100644 src/Utility/SupportedModuleLoader.php delete mode 100644 tests/Commands/Module/Sync/MetadataTest.php delete mode 100644 tests/Utility/SupportedModuleLoaderTest.php diff --git a/README.md b/README.md index 1e6fd4d..b98bbbd 100644 --- a/README.md +++ b/README.md @@ -168,48 +168,13 @@ information: - `{{ logs }}` will inject the commit logs with before/after delimiters, so they can be updated later without destroying any other changes to the contents. -## Synchronising data to supported modules +## GitHub API ratelimit -Cow includes commands to help synchronise standardised data to all -[commercially supported modules](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/): - -* `cow github:synclabels` Pushes a centralised list of labels to all supported module GitHub repositories -* `cow github:ratelimit` Check your current GitHub API rate limiting status (sync commands can use this up quickly) +Run `cow github:ratelimit` to check your current GitHub API rate limiting status **Note:** All GitHub API commands require a `GITHUB_ACCESS_TOKEN` environment variable to be set before they can be used. It can be in the .env file (see [dev mode](#dev_mode)). -### Labels - -[Centralised label configuration](https://github.com/silverstripe/supported-modules/blob/gh-pages/labels.json) can be -pushed out to all [supported modules](https://github.com/silverstripe/supported-modules/blob/gh-pages/modules.json) -using the `cow github:synclabels` command. - -This command takes an optional argument to specify which module(s) to update: - -* `modules` Optionally sync to specific modules (comma delimited) - -If the `modules` argument is not provided, the list of supported modules will be loaded from the `supported-modules` -GitHub repository. You can then confirm the list, before you will be shown a list of the labels that will be sync'd -and finally all labels will either be created or updated on the target repositories. - -This command can max out your GitHub API rate limiting credits, so use it sparingly. If you exceed the limit you may -need to go and make a coffee and come back in an hour (check current rate limits with `cow github:ratelimit`). - -### Metadata files - -[File templates](https://github.com/silverstripe/supported-modules/tree/gh-pages/templates) for supported modules can -be synchronised out to all supported modules using the `module:sync:metadata` command. - -This command will pull the latest version from the supported-modules repository, write the contents to each repository -then stage, commit and push directly to the default branch. - -This command takes an optional argument to skip the clone/pull of each repository beforehand: - -* `--skip-update` Optionally skip the clone/fetch/pull for each repository before running the sync - -You will need `git` available in your system path, as well as write permission to push to each repository. - ## Schema The [cow schema file](cow.schema.json) is in the root of this project. diff --git a/src/Application.php b/src/Application.php index 2acc25b..8c58e4c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -3,7 +3,6 @@ namespace SilverStripe\Cow; use SilverStripe\Cow\Commands; -use SilverStripe\Cow\Utility\SupportedModuleLoader; use SilverStripe\Cow\Utility\Config; use SilverStripe\Cow\Utility\GitHubApi; use SilverStripe\Cow\Utility\Twig; @@ -92,7 +91,6 @@ protected function getDefaultCommands(): array // Create dependencies $githubApi = new GitHubApi(); - $supportedModuleLoader = new SupportedModuleLoader(); // What is this cow doing in here, stop it, get out $commands[] = new Commands\MooCommand(); @@ -112,15 +110,11 @@ protected function getDefaultCommands(): array $commands[] = new Commands\Release\Release(); $commands[] = new Commands\Release\Publish(); - // Module commands - $commands[] = new Commands\Module\Sync\Metadata($supportedModuleLoader); - // Schema commands $commands[] = new Commands\Schema\Validate(); // GitHub commands $commands[] = new Commands\GitHub\RateLimit($githubApi); - $commands[] = new Commands\GitHub\SyncLabels($supportedModuleLoader, $githubApi); return $commands; } diff --git a/src/Commands/GitHub/SyncLabels.php b/src/Commands/GitHub/SyncLabels.php deleted file mode 100644 index fad833b..0000000 --- a/src/Commands/GitHub/SyncLabels.php +++ /dev/null @@ -1,226 +0,0 @@ -supportedModuleLoader = $supportedModuleLoader; - $this->github = $github; - } - - protected function configureOptions() - { - $this->addArgument( - 'modules', - InputArgument::IS_ARRAY, - 'Optionally sync to specific modules (space delimited)' - ); - } - - protected function fire() - { - $io = new SymfonyStyle($this->input, $this->output); - - $modules = $this->input->getArgument('modules'); - if (empty($modules)) { - // Loading data and confirming steps with user - $io->section('Loading supported modules'); - $modules = $this->supportedModuleLoader->getModules(); - } - - if (!$this->confirmModules($io, $modules)) { - return; - } - - $labelConfig = $this->loadLabelConfig(); - if (!$this->confirmLabels($io, $labelConfig)) { - return; - } - - // Proceed with synchronisation - $result = ['success' => 0, 'error' => 0]; - $current = 0; - $total = count($modules); - foreach ($modules as $githubSlug) { - $io->text(++$current . '/' . $total . ': Processing ' . $githubSlug . '...'); - - // Set the progress bar: total is the number of label operations to make - $io->progressStart(array_sum(array_map('count', $labelConfig))); - - try { - $this->syncLabelsToModule($githubSlug, $labelConfig, $io); - $result['success']++; - } catch (Exception $ex) { - $io->error('Error updating ' . $githubSlug . ': ' . $ex->getMessage()); - $result['error']++; - } - - $io->progressFinish(); - } - $result['total'] = array_sum($result); - - $io->success('Finished! ' . $result['success'] . '/' . $result['total'] . ' succeeded.'); - } - - /** - * Show a summary of the modules that are going to be processed and ask the user for confirmation before - * proceeding. - * - * @param SymfonyStyle $io - * @param array $modules - */ - protected function confirmModules(SymfonyStyle $io, array $modules) - { - // Show summary table - $rows = []; - foreach ($modules as $github) { - $rows[] = [$github]; - } - $io->table(['GitHub repo'], $rows); - - return $io->confirm('Continue with sync?', true); - } - - /** - * Show a summary of the labels that are going to be synchronised and ask for confirmation before proceeding - * - * @param SymfonyStyle $io - * @param array $labelData - */ - protected function confirmLabels(SymfonyStyle $io, array $labels) - { - $io->text('The following labels will be pushed to each repository:'); - // Default labels - $rows = []; - foreach ($labels['default_labels'] as $label => $hexCode) { - $rows[] = [$label, $hexCode]; - } - $io->table(['Label', 'Hex code'], $rows); - - // Renaming - $io->text('The following labels will be renamed:'); - foreach ($labels['rename_labels'] as $old => $new) { - $io->writeln(' * ' . $old . ' => ' . $new . ''); - } - $io->newLine(); - - // Removing - $io->text('The following labels will be deleted:'); - foreach ($labels['remove_labels'] as $label) { - $io->writeln(' * ' . $label . ''); - } - $io->newLine(); - - return $io->confirm('Continue with sync?', true); - } - - /** - * @return array - * @throws UnexpectedValueException - */ - protected function loadLabelConfig() - { - $labels = $this->supportedModuleLoader->getLabels(); - if (empty($labels)) { - throw new UnexpectedValueException('labels.json data could not be loaded!'); - } - return $labels; - } - - /** - * Given a GitHub repository slug and an array of labels, synchronise them to GitHub - * - * @param string $githubSlug - * @param array $labelConfig - * @param SymfonyStyle $io - */ - protected function syncLabelsToModule($githubSlug, array $labelConfig, SymfonyStyle $io) - { - /** @var LabelsApi $labelsApi */ - $labelsApi = $this->github->getClient()->api('issue')->labels(); - - list ($organisation, $repository) = explode('/', $githubSlug); - - // Rename labels - foreach ($labelConfig['rename_labels'] as $oldLabel => $newLabel) { - $io->progressAdvance(); - - // Assign white as a placeholder, it'll be updated further down - try { - $labelsApi->update($organisation, $repository, $oldLabel, $newLabel, 'FFFFFF'); - } catch (Exception $ex) { - if (strpos($ex->getMessage(), 'Not Found') === false) { - // Only log messages that aren't "Not Found", which come when the labels don't exist - $io->error($ex->getMessage()); - } - } - } - - // (Create and) update colours on labels - foreach ($labelConfig['default_labels'] as $label => $hexCode) { - $io->progressAdvance(); - - try { - $exists = $labelsApi->show($organisation, $repository, $label); - } catch (Exception $ex) { - $exists = false; - } - - if ($exists) { - // Existing, update - $labelsApi->update($organisation, $repository, $label, $label, $hexCode); - continue; - } - // Create new label - $labelsApi->create($organisation, $repository, [ - 'name' => $label, - 'color' => $hexCode, - ]); - } - - // Delete labels - foreach ($labelConfig['remove_labels'] as $label) { - $io->progressAdvance(); - - try { - $labelsApi->deleteLabel($organisation, $repository, $label); - } catch (Exception $ex) { - if (strpos($ex->getMessage(), 'Not Found') === false) { - // Only log messages that aren't "Not Found", which come when the labels don't exist - $io->error($ex->getMessage()); - } - } - } - } -} diff --git a/src/Commands/Module/AbstractSyncCommand.php b/src/Commands/Module/AbstractSyncCommand.php deleted file mode 100644 index 1f06ac8..0000000 --- a/src/Commands/Module/AbstractSyncCommand.php +++ /dev/null @@ -1,163 +0,0 @@ -setSupportedModuleLoader($supportedModuleLoader); - } - - /** - * Either clones or pulls a shallow cloned copy of each of the supported modules - */ - protected function syncRepositories() - { - $repositories = $this->getSupportedModuleLoader()->getModules(); - $baseDir = $this->getBaseDir(); - $this->output->writeln('Temporary directory: ' . $baseDir . ''); - - foreach ($repositories as $repository) { - $repositoryHash = sha1($repository); - - // Temporary: only process SilverStripe repos - // @todo should this be part of the module configuration? e.g. "sync_files": true - if (substr($repository, 0, 12) !== 'silverstripe') { - $this->output->writeln('Skipping ' . $repository . ''); - continue; - } - - if ($this->tempFolderExists($baseDir . $repositoryHash)) { - $this->output->writeln('Updating ' . $repository . '...'); - $this->updateRepository($repository); - } else { - $this->output->writeln('Cloning ' . $repository . '...'); - $this->cloneRepository($repository); - } - } - - $this->output->writeln('All repositories updated.'); - } - - /** - * Returns the base directory for storing Git repositories. Will create it if it doesn't exist yet. - * - * @return string - */ - protected function getBaseDir() - { - $baseDir = __DIR__ . '/../../../temp/'; - if (!is_dir($baseDir)) { - mkdir($baseDir); - } - return realpath($baseDir) . '/'; - } - - /** - * @param string $folder - * @return bool - */ - protected function tempFolderExists($folder) - { - return is_dir($folder); - } - - /** - * Updates an existing Git repository - * - * @param string $repository - */ - protected function updateRepository($repository) - { - $process = new Process([ - '/usr/bin/env', - 'git', - 'pull', - ]); - $process->setWorkingDirectory($this->getRepositoryPath($repository)); - - $this->getHelper('process')->run($this->output, $process); - } - - /** - * Clones a Git repository - * - * @param string $repository - */ - protected function cloneRepository($repository) - { - $process = new Process([ - '/usr/bin/env', - 'git', - 'clone', - '--depth', - '1', - $this->getRepositoryUrl($repository), - $this->getRepositoryPath($repository), - ]); - - $this->getHelper('process')->run($this->output, $process); - } - - /** - * @param string $repository - * @return string - */ - protected function getRepositoryPath($repository) - { - return $this->getBaseDir() . $this->getRepositoryHash($repository); - } - - /** - * Returns the GitHub repository URL. Uses SSH protocol. - * - * @param string $repository - * @return string - */ - protected function getRepositoryUrl($repository) - { - return 'git@github.com:' . $repository; - } - - /** - * @param string $repository - * @return string - */ - protected function getRepositoryHash($repository) - { - return sha1($repository); - } - - /** - * @param SupportedModuleLoader $supportedModuleLoader - * @return $this - */ - public function setSupportedModuleLoader(SupportedModuleLoader $supportedModuleLoader) - { - $this->supportedModuleLoader = $supportedModuleLoader; - return $this; - } - - /** - * @return SupportedModuleLoader - */ - public function getSupportedModuleLoader() - { - return $this->supportedModuleLoader; - } -} diff --git a/src/Commands/Module/Sync/Metadata.php b/src/Commands/Module/Sync/Metadata.php deleted file mode 100644 index 1cc3d81..0000000 --- a/src/Commands/Module/Sync/Metadata.php +++ /dev/null @@ -1,157 +0,0 @@ -addOption( - 'skip-update', - null, - InputOption::VALUE_NONE, - 'Skip fetching latest repository changes' - ); - } - - protected function fire() - { - // Ensure that all repositories are available and up to date - if (!$this->input->getOption('skip-update')) { - $this->syncRepositories(); - } - $repositories = $this->getSupportedModuleLoader()->getModules(); - - /** @var QuestionHelper $questionHelper */ - $questionHelper = $this->getHelper('question'); - - foreach (self::SYNC_FILES as $filename) { - $data = $this->getSupportedModuleLoader()->getRemoteData($filename); - $baseFilename = basename($filename); - - // Confirm diff before proceeding - $this->output->writeln($data); - $question = new ConfirmationQuestion( - 'Confirm file contents for ' . $baseFilename . '? ' - ); - $confirm = $questionHelper->ask($this->input, $this->output, $question); - - if (!$confirm) { - $this->output->writeln('Skipping ' . $baseFilename . ''); - continue; - } - - foreach ($repositories as $repository) { - // @todo implement check e.g. "sync_files": true - if (substr($repository, 0, 12) !== 'silverstripe') { - continue; - } - $basePath = $this->getRepositoryPath($repository); - - $this->writeDataToFile($basePath . '/' . $baseFilename, $data); - $this->stageFile($basePath, $baseFilename); - if (!$this->hasChanges($basePath)) { - continue; - } - $this->commitChanges($basePath, $baseFilename); - $this->pushChanges($basePath); - } - } - - $this->output->writeln('Done'); - } - - /** - * Writes the given data to the given filename - * - * @param string $filename - * @param string $data - */ - protected function writeDataToFile($filename, $data) - { - file_put_contents($filename, $data); - } - - /** - * Adds the given filename to stage - * - * @param string $basePath - * @param string $filename - */ - protected function stageFile($basePath, $filename) - { - $process = new Process(['/usr/bin/env', 'git', 'add', $filename, strtolower($filename)]); - $process->setWorkingDirectory($basePath); - // We don't need to know if one of the two filenames didn't exist - $process->disableOutput(); - $this->getHelper('process')->run($this->output, $process); - } - - /** - * Returns whether the given path has any staged changes in it - * - * @param string $basePath - * @return bool - */ - protected function hasChanges($basePath) - { - $process = new Process(['/usr/bin/env', 'git', 'diff', '--staged']); - $process->setWorkingDirectory($basePath); - $process->run(); - $result = $process->getOutput(); - return trim($result) !== ''; - } - - /** - * Commit stages changes - * - * @param string $basePath - * @param string $filename - */ - protected function commitChanges($basePath, $filename) - { - $process = new Process(['/usr/bin/env', 'git', 'commit', '-m', 'Update ' . $filename]); - $process->setWorkingDirectory($basePath); - $this->getHelper('process')->run($this->output, $process); - } - - /** - * Pushes any new commits to origin - * - * @param string $basePath - */ - protected function pushChanges($basePath) - { - if (Application::isDevMode()) { - echo "Not pushing changes because DEV_MODE is enabled\n"; - return; - } - $process = new Process(['/usr/bin/env', 'git', 'push']); - $process->setWorkingDirectory($basePath); - $this->getHelper('process')->run($this->output, $process); - } -} diff --git a/src/Utility/SupportedModuleLoader.php b/src/Utility/SupportedModuleLoader.php deleted file mode 100644 index 4f12847..0000000 --- a/src/Utility/SupportedModuleLoader.php +++ /dev/null @@ -1,106 +0,0 @@ -getRemoteData('modules.json'); - $modules = json_decode($data, true) ?: []; - - if ($this->getFilter()) { - $modules = $this->getFilter()->filter($modules); - } - - return array_column($modules, 'github'); - } - - /** - * Get the supported module labels configuration data - * - * @return array - */ - public function getLabels() - { - $data = $this->getRemoteData('labels.json'); - return json_decode($data, true) ?: []; - } - - /** - * Returns the full URL to a filename on the supported modules repository - * - * @param string $filename - * @return string - */ - public function getFilePath($filename) - { - return $this->getBaseUrl() . ltrim($filename, '/'); - } - - /** - * Gets data from the remote supported-modules repository by filename - * - * @param string $filename - * @return string - */ - public function getRemoteData($filename) - { - $data = file_get_contents($this->getFilePath($filename)); - return $data ?: ''; - } - - /** - * @param string $baseUrl - * @return $this - */ - public function setBaseUrl($baseUrl) - { - $this->baseUrl = $baseUrl; - return $this; - } - - /** - * @return string - */ - public function getBaseUrl() - { - return $this->baseUrl; - } - - /** - * @param FilterInterface $filter - * @return $this - */ - public function setFilter(FilterInterface $filter) - { - $this->filter = $filter; - return $this; - } - - /** - * @return FilterInterface - */ - public function getFilter() - { - return $this->filter; - } -} diff --git a/tests/Commands/Module/Sync/MetadataTest.php b/tests/Commands/Module/Sync/MetadataTest.php deleted file mode 100644 index d3fe75f..0000000 --- a/tests/Commands/Module/Sync/MetadataTest.php +++ /dev/null @@ -1,131 +0,0 @@ -moduleLoader = $this->getMockBuilder(SupportedModuleLoader::class) - ->setMethods(['getModules', 'getRemoteData']) - ->getMock(); - - $this->moduleLoader->expects($this->once())->method('getModules')->willReturn([ - 'silverstripe/foo', - 'someoneelse/foo', - ]); - - $this->moduleLoader->expects($this->once()) - ->method('getRemoteData') - ->with('templates/LICENSE.md') - ->willReturn('foo'); - - $this->metadata = $this->getMockBuilder(Metadata::class) - ->setMethods([ - 'syncRepositories', - 'writeDataToFile', - 'stageFile', - 'hasChanges', - 'commitChanges', - 'pushChanges', - ]) - ->setConstructorArgs([$this->moduleLoader]) - ->getMock(); - } - - public function testSyncRepositoriesByDefault() - { - $this->metadata->expects($this->once())->method('syncRepositories'); - - $output = $this->executeCommand([], ['yes']); - $this->assertStringContainsString('Done', $output); - } - - public function testDoesNotSyncRepositoriesWithSkipUpdateOption() - { - $this->metadata->expects($this->never())->method('syncRepositories'); - - $output = $this->executeCommand(['--skip-update' => true], ['yes']); - $this->assertStringContainsString('Done', $output); - } - - public function testSkippingFiles() - { - $output = $this->executeCommand([], ['no']); - $this->assertStringContainsString('Skipping LICENSE.md', $output); - $this->assertStringContainsString('Done', $output); - } - - public function testSkipThirdPartyRepositories() - { - $this->metadata->expects($this->once())->method('writeDataToFile'); - - $output = $this->executeCommand([], ['yes']); - $this->assertStringContainsString('Done', $output); - } - - public function testApplyChanges() - { - $this->metadata->expects($this->once())->method('writeDataToFile')->with($this->anything(), 'foo'); - $this->metadata->expects($this->once())->method('stageFile'); - $this->metadata->expects($this->once())->method('hasChanges')->willReturn(true); - $this->metadata->expects($this->once())->method('commitChanges'); - $this->metadata->expects($this->once())->method('pushChanges'); - - $output = $this->executeCommand([], ['yes']); - $this->assertStringContainsString('Done', $output); - } - - public function testCommitAndPushIsSkippedWithoutChanges() - { - $this->metadata->expects($this->once())->method('writeDataToFile')->with($this->anything(), 'foo'); - $this->metadata->expects($this->once())->method('stageFile'); - $this->metadata->expects($this->once())->method('hasChanges')->willReturn(false); - $this->metadata->expects($this->never())->method('commitChanges'); - $this->metadata->expects($this->never())->method('pushChanges'); - - $output = $this->executeCommand([], ['yes']); - $this->assertStringContainsString('Done', $output); - } - - /** - * Wrapper for executing a command and returning its output - * - * @param array $extraArgs - * @param array $inputs - * @return string - */ - protected function executeCommand(array $extraArgs = [], array $inputs = []) - { - $application = new Application(); - $application->add($this->metadata); - - $command = $application->find('module:sync:metadata'); - $commandTester = new CommandTester($command); - - if (!empty($inputs)) { - $commandTester->setInputs($inputs); - } - - $commandTester->execute(array_merge(['command' => $command->getName()], $extraArgs)); - return $commandTester->getDisplay(); - } -} diff --git a/tests/Utility/SupportedModuleLoaderTest.php b/tests/Utility/SupportedModuleLoaderTest.php deleted file mode 100644 index e899097..0000000 --- a/tests/Utility/SupportedModuleLoaderTest.php +++ /dev/null @@ -1,101 +0,0 @@ -getMockBuilder(SupportedModuleLoader::class) - ->setMethods(['getRemoteData']) - ->getMock(); - - $loader->expects($this->once())->method('getRemoteData')->willReturn(<<getModules(); - $this->assertContains('silverstripe/silverstripe-framework', $result, 'Supported modules are returned'); - $this->assertContains('silverstripe/silverstripe-fulltextsearch', $result, 'Supported modules are returned'); - } - - public function testGetLabels() - { - /** @var SupportedModuleLoader|PHPUnit_Framework_MockObject_MockObject $loader */ - $loader = $this->getMockBuilder(SupportedModuleLoader::class) - ->setMethods(['getRemoteData']) - ->getMock(); - - $loader->expects($this->once())->method('getRemoteData')->willReturn(<<getLabels(); - $this->assertNotEmpty($result['default_labels']); - $this->assertSame('ff0000', $result['default_labels']['affects/v3']); - - $this->assertNotEmpty($result['rename_labels']); - $this->assertSame('type/bug', $result['rename_labels']['bug']); - - $this->assertNotEmpty($result['remove_labels']); - $this->assertSame('good first issue', reset($result['remove_labels'])); - } - - public function testBrokenApiResponses() - { - /** @var SupportedModuleLoader|PHPUnit_Framework_MockObject_MockObject $loader */ - $loader = $this->getMockBuilder(SupportedModuleLoader::class) - ->setMethods(['getRemoteData']) - ->getMock(); - - $loader->expects($this->exactly(2))->method('getRemoteData')->willReturn(false); - - $this->assertSame([], $loader->getModules(), 'Broken HTTP response still returns an empty array'); - $this->assertSame([], $loader->getLabels(), 'Broken HTTP response still returns an empty array'); - } - - public function testGetFilePath() - { - $loader = new SupportedModuleLoader(); - - $this->assertSame( - 'https://raw.githubusercontent.com/silverstripe/supported-modules/gh-pages/modules.json', - $loader->getFilePath('modules.json') - ); - $this->assertSame( - 'https://raw.githubusercontent.com/silverstripe/supported-modules/gh-pages/labels.json', - $loader->getFilePath('/labels.json') - ); - } -}