From cc1c5ef0970b479fd4bd9448219951f0d3c06dbb Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Tue, 25 Jul 2023 10:17:11 +1200 Subject: [PATCH] NEW Create module-standardiser --- .editorconfig | 31 ++++ .github/workflows/ci.yml | 26 +++ .gitignore | 6 + LICENSE | 29 +++ README.md | 72 +++++++ composer.json | 10 + funcs_scripts.php | 143 ++++++++++++++ funcs_utils.php | 310 +++++++++++++++++++++++++++++++ phpunit.xml | 8 + run.php | 76 ++++++++ scripts/cms-any/editorconfig.php | 37 ++++ scripts/cms-any/license.php | 37 ++++ scripts/cms-any/scrutinizer.php | 3 + scripts/cms-any/travis.php | 3 + scripts/cms5/composer.php | 16 ++ tests/FuncsUtilsTest.php | 61 ++++++ tests/bootstrap.php | 5 + update_command.php | 148 +++++++++++++++ 18 files changed, 1021 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 funcs_scripts.php create mode 100644 funcs_utils.php create mode 100644 phpunit.xml create mode 100644 run.php create mode 100644 scripts/cms-any/editorconfig.php create mode 100644 scripts/cms-any/license.php create mode 100644 scripts/cms-any/scrutinizer.php create mode 100644 scripts/cms-any/travis.php create mode 100644 scripts/cms5/composer.php create mode 100644 tests/FuncsUtilsTest.php create mode 100644 tests/bootstrap.php create mode 100644 update_command.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..347aa2f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# For more information about the properties used in +# this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,js,json,css,scss,eslintrc,feature}] +indent_size = 2 +indent_style = space + +[composer.json] +indent_size = 4 + +# Don't perform any clean-up on thirdparty files + +[thirdparty/**] +trim_trailing_whitespace = false +insert_final_newline = false + +[admin/thirdparty/**] +trim_trailing_whitespace = false +insert_final_newline = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46090ea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + steps: + + - name: Checkout code + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + + - name: Install PHP + uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2.22.0 + with: + php-version: 7.4 + + - name: Install PHPUnit + run: wget https://phar.phpunit.de/phpunit-9.5.phar + + - name: PHPUnit + run: php phpunit-9.5.phar --verbose --colors=always diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e59406 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +_data +_modules +_tmp +vendor +.phpunit.result.cache +composer.lock diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82361bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, SilverStripe Limited - www.silverstripe.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 23c702d..df29409 100644 --- a/README.md +++ b/README.md @@ -1 +1,73 @@ # Module standardiser + +This tools standardises some files in Silverstripe modules that's intended to run on a developers laptop and create +a number of pull-requests in GitHub. + +**This tool is only intended for use by Silverstripe core committers or the Silverstripe Ltd CMS Squad** + +It will run across all modules in [supported-modules](https://github.com/silverstripe/supported-modules) list and the +relevant branch e.g. `5` will be used depending on the command-line `--branch` option that's passed in. + +It will run all scripts in the `scripts/any` folder and then run all scripts in the applicable +`scripts/` folder depending on the command-line `--branch` option that's passed in. + +## GitHub Token + +This tool creates pull-request via the GitHub API. You need to set the `MS_GITHUB_TOKEN` environment variable in order +for this to work. + +Create a new GitHub token in [https://github.com/settings/tokens/new](https://github.com/settings/tokens/new) +and only tick the `public_repo` checkbox and set it to expire in 7 days. If you do not set the correct permissions +then you will get a 404 error when attempting to create pull-requests. + +Delete this token once you have finished. + +## Usage + +```bash +git clone git@github.com:silverstripe/module-standardiser.git +cd module-standardiser +composer install +MS_GITHUB_TOKEN= php run.php update +``` + +**Example usage:** +```bash +MS_GITHUB_TOKEN=abc123 php run.php update --cms-major=5 --branch=next-minor --dry-run --only=silverstripe-config,silverstripe-assets +``` + +## Command line options: + +| Flag | Description | +| ---- | ------------| +| --cms-major=[version] | The major version of CMS to use (default: 5) | +| --branch=[type] | The branch type to use - next-minor\|next-patch (default: next-minor) | +| --only=[modules] | Only include the specified modules (without account prefix) separated by commas e.g. silverstripe-config,silverstripe-assets | +| --exclude=[modules] | Exclude the specified modules (without account prefix) separated by commas e.g. silverstripe-mfa,silverstripe-totp | +| --dry-run | Do not push to github or create pull-requests | +| --account | GitHub account to use for creating pull-requests (default: creative-commoners) | +| --no-delete | Do not delete _data and _modules directories before running | + +## GitHub API secondary rate limit + +You may hit a secondary GitHub rate limit because this tool may create too many pull-requests in a short space of time. +To help with this the tool will always output the urls of all pull-requests updated and also the repos that were +updated so you can add them to the --exclude flag on subsequent re-runs. + +## Adding new scripts + +Simply add new scripts to either `scripts/cms-` to run on a specific cms-major or `scripts/cms-any` to run +on any cms-major and they will be automatically picked up and run when the tool is run. Code in the script will be +passed through `eval()` on the module that is currently being processed. + +Make use of functions in `funcs_scripts.php` such as `write_file_if_not_exist()` and `read_file()` to access the +correct files in the module that is currently being processed and also to ensure that console output is consistent. + +Do not use functions in `funcs_utils.php` as they are not intended to be used in scripts. + +Scripts will be automatically wrapped in an anoymous function so you do not need to worry about variables crossing +over into different scripts. + +## Updating the tool when a new major version of CMS is updated + +Update the `CURRENT_CMS_MAJOR` constant in `run.php` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..aee3979 --- /dev/null +++ b/composer.json @@ -0,0 +1,10 @@ +{ + "require": { + "php": ">=7.4", + "symfony/console": "^6.3", + "symfony/process": "^6.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + } +} diff --git a/funcs_scripts.php b/funcs_scripts.php new file mode 100644 index 0000000..740094c --- /dev/null +++ b/funcs_scripts.php @@ -0,0 +1,143 @@ + instead of ->info() so that it only takes up one line instead of five + io()->writeln("$message"); +} + +/** + * Output a warning message to the console + * + * Example usage: + * warning('This is something you might want to pay attention to') + */ +function warning($message) +{ + io()->warning($message); +} diff --git a/funcs_utils.php b/funcs_utils.php new file mode 100644 index 0000000..1306dfd --- /dev/null +++ b/funcs_utils.php @@ -0,0 +1,310 @@ +error($message); + if (!running_unit_tests()) { + die; + } +} + +/** + * Write to a file after trimming the contents and adding a newline + */ +function write_file($path, $contents) +{ + if (empty($path)) { + error('Path cannot be empty'); + } + $dirname = dirname($path); + if (!file_exists($dirname)) { + error("Directory $dirname does not exist"); + } + $contents = trim($contents) . "\n"; + file_put_contents($path, $contents); + info("Wrote to $path"); +} + +/** + * Returns all the supported modules for a particular cms major version + * Will download the list if it doesn't exist + */ +function supported_modules($cmsMajor) +{ + $filename = "_data/modules-cms$cmsMajor.json"; + if (!file_exists($filename)) { + $url = "https://raw.githubusercontent.com/silverstripe/supported-modules/$cmsMajor/modules.json"; + info("Downloading $url to $filename"); + $contents = file_get_contents($url); + file_put_contents($filename, $contents); + } + $json = json_decode(file_get_contents($filename), true); + if (is_null($json)) { + $lastError = json_last_error(); + error("Could not parse from $filename - last error was $lastError"); + } + $modules = []; + foreach ($json as $module) { + $ghrepo = $module['github']; + $modules[] = [ + 'ghrepo' => $ghrepo, + 'account' => explode('/', $ghrepo)[0], + 'repo' => explode('/', $ghrepo)[1], + 'cloneUrl' => "git@github.com:$ghrepo.git", + 'branch' => max($module['branches'] ?: [-1]) + ]; + } + return $modules; +} + +/** + * Returns a list of all scripts files to run against a particular cms major version + */ +function script_files($cmsMajor) +{ + if (!ctype_digit($cmsMajor)) { + $cmsMajor = "-$cmsMajor"; + } + $scriptFiles = []; + $dir = "scripts/cms$cmsMajor"; + if (!file_exists($dir)) { + warning("$dir does not exist, no CMS $cmsMajor specific scripts will be run"); + return $scriptFiles; + } + if (!is_dir($dir)) { + error("$dir is not a directory"); + } + if ($handle = opendir($dir)) { + while (false !== ($scriptFile = readdir($handle))) { + if ('.' === $scriptFile || '..' === $scriptFile) { + continue; + } + $scriptFiles[] = "$dir/$scriptFile"; + } + closedir($handle); + } + return $scriptFiles; +} + +/** + * Runs a shell command and returns the output + */ +function cmd($cmd, $cwd) +{ + info("Running command: $cmd in $cwd"); + // using Process::fromShellCommandline() instead of new Process() so that pipes work + $process = Process::fromShellCommandline($cmd, $cwd); + $process->run(); + if (!$process->isSuccessful()) { + warning("Error running command: $cmd in $cwd"); + error("Output was: " . $process->getErrorOutput()); + } + return trim($process->getOutput()); +} + +/** + * Returns a object used to output to the console + */ +function io(): SymfonyStyle +{ + global $IN, $OUT; + return new SymfonyStyle($IN ?: new ArgvInput(), $OUT ?: new NullOutput); +} + +/** + * Removes a directory + */ +function remove_dir($dirname) +{ + if (!file_exists(($dirname))) { + return; + } + if (!is_dir($dirname)) { + error("$dirname is not a directory"); + } + info("Removing $dirname"); + shell_exec("rm -rf $dirname"); +} + +/** + * Validates the users system is ready to run the script + */ +function validate_system() +{ + $token = github_token(); + if (!$token || !is_string($token)) { + error('Could not get github token - set MS_GITHUB_TOKEN environment variable'); + } + if (!cmd('which git', '.')) { + error('git is not installed'); + } +} + +/** + * Reads MS_GITHUB_TOKEN environment variable + */ +function github_token() +{ + return getenv('MS_GITHUB_TOKEN') ?: ''; +} + +/** + * Makes a request to the github API + */ +function github_api($url, $data = []) +{ + $token = github_token(); + $jsonStr = empty($data) ? '' : json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, !empty($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: silverstripe-module-standardiser', + 'Accept: application/vnd.github+json', + "Authorization: Bearer $token", + 'X-GitHub-Api-Version: 2022-11-28' + ]); + if ($jsonStr) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonStr); + } + $response = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($httpcode >= 300) { + warning("HTTP code $httpcode returned from GitHub API"); + warning($response); + error("Failure calling github api: $url"); + } + return json_decode($response, true); +} + + +function running_unit_tests() +{ + // $PRS_CREATED won't be set when running unit tests + global $PRS_CREATED; + return !isset($PRS_CREATED); +} + +/** + * Outputs a list of PRs created + * Prefixed with a dash so that it's easy to copy and paste into a parent github issue + */ +function output_prs_created() +{ + if (running_unit_tests()) { + return; + } + global $PRS_CREATED; + $io = io(); + $io->writeln(''); + $io->writeln('Pull requests created:'); + foreach ($PRS_CREATED as $pr) { + $io->writeln("- $pr"); + } + $io->writeln(''); +} + +/** + * Ouputs a list of repos that that had PRs created + * This is intended to be used when there was an error with a run (probably a secondary rate limit) and then + * copy pasted into the --exclude option for the next run + */ +function output_repos_with_prs_created() +{ + if (running_unit_tests()) { + return; + } + global $REPOS_WITH_PRS_CREATED; + $io = io(); + $io->writeln(''); + $io->writeln('Repos with pull requests created (add to --exclude if you need to re-run):'); + $io->writeln(implode(',', $REPOS_WITH_PRS_CREATED)); + $io->writeln(''); +} + +/** + * Works out which branch in a module to checkout before running scripts on it + * + * Assumes that for each module there is only a single major version per cms-major version + */ +function branch_to_checkout($branches, $currentBranch, $currentBranchCmsMajor, $cmsMajor, $branchOption) +{ + $offset = (int) $cmsMajor - (int) $currentBranchCmsMajor; + $majorTarget = (int) $currentBranch + $offset; + $branches = array_filter($branches, fn($branch) => preg_match('#^[0-9\.]+$#', $branch)); + usort($branches, 'version_compare'); + $branches = array_reverse($branches); + switch ($branchOption) { + case 'next-patch': + $branchToCheckout = array_values(array_filter( + $branches, + fn($branch) => preg_match("#^$majorTarget.[0-9]+$#", $branch) + ))[0] ?? null; + break; + case 'next-minor': + default: + $branchToCheckout = $majorTarget; + } + return (string) $branchToCheckout; +} + +function current_branch_cms_major( + // this param is only used for unit testing + string $composerJson = '' +) { + // read __composer.json of the current branch + $contents = $composerJson ?: read_file('composer.json'); + + $json = json_decode($contents); + if (is_null($json)) { + $lastError = json_last_error(); + error("Could not parse from composer.json - last error was $lastError"); + } + $matchedOnBranchThreeLess = false; + $version = preg_replace('#[^0-9\.]#', '', $json->require->{'silverstripe/framework'} ?? ''); + if (!$version) { + $version = preg_replace('#[^0-9\.]#', '', $json->require->{'silverstripe/cms'} ?? ''); + } + if (!$version) { + $version = preg_replace('#[^0-9\.]#', '', $json->require->{'silverstripe/mfa'} ?? ''); + } + if (!$version) { + $version = preg_replace('#[^0-9\.]#', '', $json->require->{'silverstripe/assets'} ?? ''); + $matchedOnBranchThreeLess = true; + } + $cmsMajor = ''; + if (preg_match('#^([0-9]+)+\.?[0-9]*$#', $version, $matches)) { + $cmsMajor = $matches[1]; + if ($matchedOnBranchThreeLess) { + $cmsMajor += 3; + } + } else { + $phpVersion = $json->require->{'php'} ?? ''; + if (substr($phpVersion,0, 4) === '^7.4') { + $cmsMajor = 4; + } elseif (substr($phpVersion,0, 4) === '^8.1') { + $cmsMajor = 5; + } + } + if ($cmsMajor === '') { + error('Could not work out what the current CMS major version is'); + } + return (string) $cmsMajor; +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..f5e111b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests + + + diff --git a/run.php b/run.php new file mode 100644 index 0000000..e6e10a1 --- /dev/null +++ b/run.php @@ -0,0 +1,76 @@ +register('update') + ->setDescription('The main script of module-standardiser') + ->addOption( + 'cms-major', + null, + InputOption::VALUE_REQUIRED, + 'The CMS major version to use (default: '. CURRENT_CMS_MAJOR .')' + ) + ->addOption( + 'branch', + null, + InputOption::VALUE_REQUIRED, + 'The branch type to use - ' . implode('|', BRANCH_OPTIONS) . ' (default: ' . DEFAULT_BRANCH . ')' + ) + ->addOption( + 'only', + null, + InputOption::VALUE_REQUIRED, + 'Only include the specified modules (without account prefix) separated by commas ' + . 'e.g. silverstripe-config,silverstripe-assets' + ) + ->addOption( + 'exclude', + null, + InputOption::VALUE_REQUIRED, + 'Exclude the specified modules (without account prefix) separated by commas ' + . 'e.g. silverstripe-mfa,silverstripe-totp' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Do not push to github or create pull-requests' + ) + ->addOption( + 'account', + null, + InputOption::VALUE_REQUIRED, + 'GitHub account to use for creating pull-requests (default: ' . DEFAULT_ACCOUNT . ')' + ) + ->addOption( + 'no-delete', + null, + InputOption::VALUE_NONE, + 'Do not delete _data and _modules directories before running' + ) + ->setCode($updateCommand); +$app->run(); diff --git a/scripts/cms-any/editorconfig.php b/scripts/cms-any/editorconfig.php new file mode 100644 index 0000000..d3ee8fa --- /dev/null +++ b/scripts/cms-any/editorconfig.php @@ -0,0 +1,37 @@ +assertSame($expected, $actual); + } + + public function provideBranchToCheckout() + { + $branches = ['1.5', '1.6', '1', '2.0', '2.1' , '2.2', '2', '3', 'pulls/2.3/something', 'random']; + return [ + ['2', $branches, '2', '5', '5', 'next-minor'], + ['2.2', $branches, '2', '5', '5', 'next-patch'], + ['1', $branches, '2', '5', '4', 'next-minor'], + ['1.6', $branches, '2', '5', '4', 'next-patch'], + ['2', $branches, '1', '4', '5', 'next-minor'], + ['2.2', $branches, '1', '4', '5', 'next-patch'], + ['3', $branches, '1', '4', '6', 'next-minor'], + ]; + } + + /** + * @dataProvider provideCurrentBranchCmsMajor + */ + public function testCurrentBranchCmsMajor($expected, $composerJson) + { + $actual = current_branch_cms_major($composerJson); + $this->assertSame($expected, $actual); + } + + public function provideCurrentBranchCmsMajor() + { + return [ + ['4', json_encode(['require' => ['silverstripe/framework' => '^4.13']])], + ['5', json_encode(['require' => ['silverstripe/framework' => '^5.0']])], + ['6', json_encode(['require' => ['silverstripe/framework' => '^6']])], + ['5', json_encode(['require' => ['silverstripe/cms' => '^5']])], + ['5', json_encode(['require' => ['silverstripe/mfa' => '^5']])], + ['4', json_encode(['require' => ['silverstripe/assets' => '^1']])], + ['5', json_encode(['require' => ['silverstripe/assets' => '^2']])], + ['4', json_encode(['require' => ['php' => '^7.4']])], + ['5', json_encode(['require' => ['php' => '^8.1']])], + ['', json_encode(['require' => ['silverstripe/lorem-ipsum' => '^2']])], + ]; + } +} + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..5497497 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +getOption('no-delete')) { + remove_dir(DATA_DIR); + remove_dir(MODULES_DIR); + } + if (!file_exists(DATA_DIR)) { + mkdir(DATA_DIR); + } + if (!file_exists(MODULES_DIR)) { + mkdir(MODULES_DIR); + } + + // branch + $branchOption = $input->getOption('branch') ?: DEFAULT_BRANCH; + if (!in_array($branchOption, BRANCH_OPTIONS)) { + error(sprintf('Invalid branch option - must be one of: %s', implode('|', BRANCH_OPTIONS))); + } + + // CMS major version to use + $cmsMajor = $input->getOption('cms-major') ?: CURRENT_CMS_MAJOR; + + // modules + $modules = supported_modules($cmsMajor); + if ($input->getOption('only')) { + $only = explode(',', $input->getOption('only')); + $modules = array_filter($modules, function ($module) use ($only) { + return in_array($module['repo'], $only); + }); + } + if ($input->getOption('exclude')) { + $exclude = explode(',', $input->getOption('exclude')); + $modules = array_filter($modules, function ($module) use ($exclude) { + return !in_array($module['repo'], $exclude); + }); + } + + // script files + $scriptFiles = array_merge( + script_files('any'), + script_files($cmsMajor), + ); + + // clone repos & run scripts + foreach ($modules as $module) { + $account = $module['account']; + $repo = $module['repo']; + $cloneUrl = $module['cloneUrl']; + $MODULE_DIR = MODULES_DIR . "/$repo"; + if (!file_exists($MODULE_DIR)) { + cmd("git clone $cloneUrl", MODULES_DIR); + } + + // get all branches + $allBranches = explode("\n", cmd('git branch -r', $MODULE_DIR)); + $allBranches = array_map(fn($branch) => trim(str_replace('origin/', '', $branch)), $allBranches); + + // reset to the default branch so that we can then calculate the correct branch to checkout + // this is needed for scenarios where we may be on something unparsable like pulls/5/lorem-ipsum + $cmd = "git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"; + $defaultBranch = cmd($cmd, $MODULE_DIR); + cmd("git checkout $defaultBranch", $MODULE_DIR); + + // checkout the branch to run scripts over + $currentBranch = cmd('git rev-parse --abbrev-ref HEAD', $MODULE_DIR); + $currentBranchCmsMajor = current_branch_cms_major(); + $branchToCheckout = branch_to_checkout( + $allBranches, + $currentBranch, + $currentBranchCmsMajor, + $cmsMajor, + $branchOption + ); + if (!in_array($branchToCheckout, $allBranches)) { + error("Could not find branch to checkout for $repo using --branch=$branchOption"); + } + cmd("git checkout $branchToCheckout", $MODULE_DIR); + + // create a new branch used for the pull-request + $timestamp = time(); + $prBranch = "pulls/$branchToCheckout/module-standardiser-$timestamp"; + cmd("git checkout -b $prBranch", $MODULE_DIR); + + // run scripts + foreach ($scriptFiles as $scriptFile) { + $contents = file_get_contents($scriptFile); + $contents = str_replace('getOption('account') ?? DEFAULT_ACCOUNT; + $origin = cmd('git remote get-url origin', $MODULE_DIR); + $prOrigin = str_replace("git@github.com:$account", "git@github.com:$prAccount", $origin); + // remove any existing pr-remote - need to do this in case we change the account option + $remotes = explode("\n", cmd('git remote', $MODULE_DIR)); + if (in_array('pr-remote', $remotes)) { + cmd('git remote remove pr-remote', $MODULE_DIR); + } + cmd("git remote add pr-remote $prOrigin", $MODULE_DIR); + + // commit changes, push changes and create pull-request + $status = cmd('git status', $MODULE_DIR); + if (strpos($status, 'nothing to commit') !== false) { + info("No changes to commit for $repo"); + } else { + cmd('git add .', $MODULE_DIR); + cmd("git commit -m '" . PR_TITLE . "'", $MODULE_DIR); + if ($input->getOption('dry-run')) { + info('Not pushing changes or creating pull-request because --dry-run option is set'); + } else { + // push changes to pr-remote + cmd("git push -u pr-remote $prBranch", $MODULE_DIR); + // create pull-request using github api + // https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#create-a-pull-request + $responseJson = github_api("https://api.github.com/repos/$account/$repo/pulls", [ + 'title' => PR_TITLE, + 'body' => PR_DESCRIPTION, + 'head' => "$prAccount:$prBranch", + 'base' => $branchToCheckout, + ]); + $PRS_CREATED[] = $responseJson['html_url']; + $REPOS_WITH_PRS_CREATED[] = $repo; + info("Created pull-request for $repo"); + } + } + } + output_repos_with_prs_created(); + output_prs_created(); + return Command::SUCCESS; +};