diff --git a/README.md b/README.md index 557be79..59d9f5d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ relevant branch e.g. `5` will be used depending on the command-line `--branch` o 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. +This tool can also be used to standardise GitHub labels on all supported repositories. + ## GitHub Token This tool creates pull-request via the GitHub API. You need to set the `MS_GITHUB_TOKEN` environment variable in order @@ -22,12 +24,17 @@ then you will get a 404 error when attempting to create pull-requests. Delete this token once you have finished. -## Usage +## Installation ```bash git clone git@github.com:silverstripe/module-standardiser.git cd module-standardiser composer install +``` + +## Usage - Standardising module files + +```bash MS_GITHUB_TOKEN= php run.php update ``` @@ -36,7 +43,7 @@ MS_GITHUB_TOKEN= php run.php update MS_GITHUB_TOKEN=abc123 php run.php update --cms-major=5 --branch=next-minor --dry-run --only=silverstripe-config,silverstripe-assets ``` -## Command line options: +### Command line options: | Flag | Description | | ---- | ------------| @@ -51,15 +58,15 @@ MS_GITHUB_TOKEN=abc123 php run.php update --cms-major=5 --branch=next-minor --dr **Note** that using `--branch=github-default` will only run scripts in the `scripts/default-branch` directory. -## GitHub API secondary rate limit +### 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 +### Adding new scripts -### Where to add your script +#### Where to add your script - `scripts/cms-` to run on a specific cms-major - `scripts/cms-any` to run on any cms-major @@ -76,6 +83,28 @@ Do not use functions in `funcs_utils.php` as they are not intended to be used in Scripts will be automatically wrapped in an anoymous function so you do not need to worry about variables crossing over into different scripts. +## Usage - Standardising GitHub labels + +```bash +# +MS_GITHUB_TOKEN= php run.php labels +``` + +**Example usage:** +```bash +MS_GITHUB_TOKEN=abc123 php run.php labels --dry-run --only=silverstripe-config,silverstripe-assets +``` + +### Command line options: + +| Flag | Description | +| ---- | ------------| +| --cms-major=[version] | The major version of CMS to use (default: 5) | +| --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 update labels in GitHub, output to terminal only | +| --no-delete | Do not delete `_data` and `modules` directories before running | + ## Updating the tool when a new major version of CMS is updated Update the `CURRENT_CMS_MAJOR` constant in `run.php` diff --git a/funcs_utils.php b/funcs_utils.php index a25749d..260582b 100644 --- a/funcs_utils.php +++ b/funcs_utils.php @@ -295,6 +295,24 @@ function output_repos_with_prs_created() $io->writeln(''); } +/** + * Ouputs a list of repos that that had labels updated + * 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_labels_updated() +{ + if (running_unit_tests()) { + return; + } + global $REPOS_WITH_LABELS_UPDATED; + $io = io(); + $io->writeln(''); + $io->writeln('Repos with labels created (add to --exclude if you need to re-run):'); + $io->writeln(implode(',', $REPOS_WITH_LABELS_UPDATED)); + $io->writeln(''); +} + /** * Works out which branch in a module to checkout before running scripts on it * @@ -407,3 +425,38 @@ function current_branch_cms_major( } return (string) $cmsMajor; } + +function setup_directories($input) { + if (!$input->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); + } +} + +function filtered_modules($cmsMajor, $input) { + $modules = supported_modules($cmsMajor); + if ($cmsMajor === CURRENT_CMS_MAJOR) { + // only include extra_repositories() when using the current CMS major version because the extra rexpositories + // don't have multi majors branches supported e.g. gha-generate-matrix + $modules = array_merge($modules, extra_repositories()); + } + 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); + }); + } + return $modules; +} diff --git a/labels_command.php b/labels_command.php new file mode 100644 index 0000000..a7ada30 --- /dev/null +++ b/labels_command.php @@ -0,0 +1,290 @@ + 'complexity/low', +// 'effort/medium' => 'complexity/medium', +// 'effort/hard' => 'complexity/high', +// 'change/major' => 'type/api-break', +// 'type/api-change' => 'type/api-break' +// ]; + +// Do not prefix color with hash +const LABEL_COLOR = [ + 'affects/v4' => '5319e7', + 'affects/v5' => '0e8a16', + 'complexity/low' => 'c2e0c6', + 'complexity/medium' => 'fef2c0', + 'complexity/high' => 'f9d0c4', + 'Epic' => '3e4b9e', + 'impact/low' => 'fef2c0', + 'impact/medium' => 'f7c6c7', + 'impact/high' => 'eb6420', + 'impact/critical' => 'e11d21', + 'rfc/accepted' => 'dddddd', + 'rfc/draft' => 'dddddd', + 'type/api-break' => '1d76db', + 'type/bug' => 'd93f0b', + 'type/docs' => '02d7e1', + 'type/enhancement' => '0e8a16', + 'type/userhelp' => 'c5def5', + 'type/UX' => '006b75', + // + 'type/other' => 'cccccc', +]; + +// const LABELS_REMOVE = [ +// 'change/patch', +// 'change/minor', +// 'feedback-required/author', +// 'feedback-required/core-team', +// 'affects/v3', +// 'type/frontend', +// # +// 'dependencies', +// 'good first issue', +// 'javascript', +// 'wontfix', +// 'invalid', +// 'question', +// 'duplicate', +// 'enhancement', +// 'help wanted', +// 'bug', +// 'documentation', +// 'affects/mobile', +// 'blocker', +// 'WIP', +// 'feature', +// 'discussion', +// 'v4', +// 'required-for-merge', +// 'post-release', +// 'api', +// 'hacktoberfest-accepted', +// ]; + +$labelsCommand = function(InputInterface $input, OutputInterface $output): int { + // This is the code that is executed when running the 'labels' command + + // variables + global $OUT, $REPOS_WITH_LABELS_UPDATED; + $OUT = $output; + + // validate system is ready + validate_system(); + + // setup directories + setup_directories($input); + + // CMS major version to use + $cmsMajor = $input->getOption('cms-major') ?: CURRENT_CMS_MAJOR; + + // modules + $modules = filtered_modules($cmsMajor, $input); + + // update labels + foreach ($modules as $module) { + $account = $module['account']; + $repo = $module['repo']; + + // https://github.com/emteknetnz/issue-relabeller/blob/main/run.php + + // fetch labels + // https://api.github.com/repos/OWNER/REPO/labels + $labelsArr = github_api("https://api.github.com/repos/$account/$repo/labels"); + + foreach ($labelsArr as $labelArr) { + /* + array(7) { + 'id' => int(427423377) + 'node_id' => string(24) "MDU6TGFiZWw0Mjc0MjMzNzc=" + 'url' => string(79) "https://api.github.com/repos/silverstripe/silverstripe-config/labels/affects/v4" + 'name' => string(10) "affects/v4" + 'color' => string(6) "5319e7" + 'default' => bool(false) + 'description' => NULL + } + */ + $name = $labelArr['name']; // e.g. 'affects/v4' + if (!array_key_exists($name, LABEL_COLOR)) { + // delete + continue; + } + if (LABEL_COLOR[$name] !== $labelArr['color']) { + // update + continue; + } + } + + $REPOS_WITH_LABELS_UPDATED[] = $repo; + + if ($input->getOption('dry-run')) { + info('Not updating labels on GitHub because --dry-run option is set'); + continue; + } + + } + output_repos_with_labels_updated(); + + // // clone repos & run scripts + // foreach ($modules as $module) { + // $account = $module['account']; + // $repo = $module['repo']; + // $cloneUrl = $module['cloneUrl']; + // $MODULE_DIR = MODULES_DIR . "/$repo"; + // // clone repo + // // always clone the actual remote even when doing update-prs even though this is slower + // // reason is because we read origin in .git/config to workout the actual $account in + // // module_account() which is very important when setting up github-action crons + // if (!file_exists($MODULE_DIR)) { + // cmd("git clone $cloneUrl", MODULES_DIR); + // } + // // set git remote + // $prAccount = $input->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); + + // if ($input->getOption('update-prs')) { + // // checkout latest existing pr branch + // cmd('git fetch pr-remote', $MODULE_DIR); + // $allBranches = explode("\n", cmd('git branch -r', $MODULE_DIR)); + // // example branch name: pulls/5/module-standardiser-1691550112 + // $allBranches = array_map('trim', $allBranches); + // $allBranches = array_filter($allBranches, function($branch) { + // return preg_match('#^pr\-remote/pulls/[0-9\.]+/module\-standardiser\-[0-9]{10}$#', $branch); + // }); + // if (empty($allBranches)) { + // warning("Could not find an existing PR branch for $repo - skipping"); + // continue; + // } + // // sort so that the branch with the highest timestamp goes to position 0 in the array + // usort($allBranches, function($a, $b) { + // return (substr($a, -10) <=> substr($b, -10)) * -1; + // }); + // $branchToCheckout = $allBranches[0]; + // $branchToCheckout = preg_replace('#^pr\-remote/#', '', $branchToCheckout); + // $prBranch = $branchToCheckout; + // $allPRs = github_api("https://api.github.com/repos/$account/$repo/pulls?per_page=100"); + // $allPRs = array_filter($allPRs, function($pr) use($prBranch) { + // return $pr['title'] === PR_TITLE && $pr['head']['ref'] === $prBranch && $pr['state'] === 'open'; + // }); + // if (count($allPRs) < 1) { + // warning("Could not find an existing open PR for $repo for branch $prBranch - skipping"); + // continue; + // } + // } else { + // // 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); + // // ensure that we're on a standard next-minor style branch + // if (!ctype_digit($currentBranch)) { + // $tmp = array_filter($allBranches, fn($branch) => ctype_digit($branch)); + // if (empty($tmp)) { + // error('Could not find a next-minor style branch'); + // } + // $currentBranch = max($tmp); + // cmd("git checkout $currentBranch", $MODULE_DIR); + // } + // $currentBranchCmsMajor = current_branch_cms_major(); + // $branchToCheckout = branch_to_checkout( + // $allBranches, + // $defaultBranch, + // $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); + + // // ensure that this branch actually supports the cmsMajor we're targetting + // if ($branchOption !== 'github-default' && current_branch_cms_major() !== $cmsMajor) { + // error("Branch $branchToCheckout does not support CMS major version $cmsMajor"); + // } + + // // create a new branch used for the pull-request + // if (!$input->getOption('update-prs')) { + // $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('update-prs')) { + // // squash on to existing commit + // $lastCommitMessage = cmd('git log -1 --pretty=%B', $MODULE_DIR); + // if ($lastCommitMessage !== PR_TITLE) { + // error("Last commit message \"$lastCommitMessage\" does not match PR_TITLE \"" . PR_TITLE . "\""); + // } + // cmd("git commit --amend --no-edit", $MODULE_DIR); + // } else { + // // create new commit + // 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'); + // continue; + // } + // // push changes to pr-remote + // // force pushing for cases when doing update-prs + // // double make check we're on a branch that we are willing to force push + // $currentBranch = cmd('git rev-parse --abbrev-ref HEAD', $MODULE_DIR); + // if (!preg_match('#^pulls/([0-9\.]+|master|main)/module\-standardiser\-[0-9]{10}$#', $currentBranch)) { + // error("Branch $currentBranch is not a pull-request branch"); + // } + // cmd("git push -f -u pr-remote $prBranch", $MODULE_DIR); + // // create pull-request using github api + // if (!$input->getOption('update-prs')) { + // // 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']; + // info("Created pull-request for $repo"); + // } + // $REPOS_WITH_PRS_CREATED[] = $repo; + // } + // output_repos_with_prs_created(); + // output_prs_created(); + return Command::SUCCESS; +}; diff --git a/run.php b/run.php index 34a028a..2291889 100644 --- a/run.php +++ b/run.php @@ -4,6 +4,7 @@ include 'funcs_scripts.php'; include 'funcs_utils.php'; include 'update_command.php'; +include 'labels_command.php'; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\InputOption; @@ -25,61 +26,81 @@ $REPOS_WITH_PRS_CREATED = []; $OUT = null; +// options +$optionCmsMajor = [ + 'cms-major', + null, + InputOption::VALUE_REQUIRED, + 'The CMS major version to use (default: '. CURRENT_CMS_MAJOR .')' +]; +$optionBranch = [ + 'branch', + null, + InputOption::VALUE_REQUIRED, + 'The branch type to use - ' . implode('|', BRANCH_OPTIONS) . ' (default: ' . DEFAULT_BRANCH . ')' +]; +$optionOnly = [ + 'only', + null, + InputOption::VALUE_REQUIRED, + 'Only include the specified modules (without account prefix) separated by commas ' + . 'e.g. silverstripe-config,silverstripe-assets' +]; +$optionExclude = [ + 'exclude', + null, + InputOption::VALUE_REQUIRED, + 'Exclude the specified modules (without account prefix) separated by commas ' + . 'e.g. silverstripe-mfa,silverstripe-totp' +]; +$optionDryRun = [ + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Do not push to github or create pull-requests' +]; +$optionAccount = [ + 'account', + null, + InputOption::VALUE_REQUIRED, + 'GitHub account to use for creating pull-requests (default: ' . DEFAULT_ACCOUNT . ')' +]; +$optionNoDelete = [ + 'no-delete', + null, + InputOption::VALUE_NONE, + 'Do not delete _data and _modules directories before running' +]; +$optionUpdatePrs = [ + 'update-prs', + null, + InputOption::VALUE_NONE, + 'Checkout out and update the latest open PR instead of creating a new one' +]; + $app = new Application(); + $app->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' - ) - ->addOption( - 'update-prs', - null, - InputOption::VALUE_NONE, - 'Checkout out and update the latest open PR instead of creating a new one' - ) + ->addOption(...$optionCmsMajor) + ->addOption(...$optionBranch) + ->addOption(...$optionOnly) + ->addOption(...$optionExclude) + ->addOption(...$optionDryRun) + ->addOption(...$optionAccount) + ->addOption(...$optionNoDelete) + ->addOption(...$optionUpdatePrs) ->setCode($updateCommand); +$app->register('labels') + ->setDescription('Script to set labels on all repos') + ->addOption(...$optionCmsMajor) + ->addOption(...$optionOnly) + ->addOption(...$optionExclude) + ->addOption(...$optionDryRun) + ->addOption(...$optionNoDelete) + ->setCode($labelsCommand); + try { $app->run(); } catch (Error|Exception $e) { diff --git a/update_command.php b/update_command.php index 2736c97..81ed009 100644 --- a/update_command.php +++ b/update_command.php @@ -15,16 +15,7 @@ validate_system(); // setup directories - if (!$input->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); - } + setup_directories($input); // branch $branchOption = $input->getOption('branch') ?: DEFAULT_BRANCH; @@ -36,24 +27,7 @@ $cmsMajor = $input->getOption('cms-major') ?: CURRENT_CMS_MAJOR; // modules - $modules = supported_modules($cmsMajor); - if ($cmsMajor === CURRENT_CMS_MAJOR) { - // only include extra_repositories() when using the current CMS major version because the extra rexpositories - // don't have multi majors branches supported e.g. gha-generate-matrix - $modules = array_merge($modules, extra_repositories()); - } - 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); - }); - } + $modules = filtered_modules($cmsMajor, $input); // script files if ($branchOption === 'github-default') {