From 1acead093df032cd3dd879bdddc49c26c72786e2 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Wed, 12 Jul 2023 16:10:01 +1200 Subject: [PATCH] Create action --- .github/workflows/auto-tag.yml | 12 ++ .github/workflows/ci.yml | 26 +++++ .gitignore | 2 + LICENSE | 29 +++++ README.md | 16 ++- action.yml | 199 +++++++++++++++++++++++++++++++++ branches.php | 9 ++ funcs.php | 111 ++++++++++++++++++ phpunit.xml | 8 ++ tests/BranchesTest.php | 88 +++++++++++++++ tests/bootstrap.php | 3 + 11 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/auto-tag.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 action.yml create mode 100644 branches.php create mode 100644 funcs.php create mode 100644 phpunit.xml create mode 100644 tests/BranchesTest.php create mode 100644 tests/bootstrap.php diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 0000000..17712c8 --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -0,0 +1,12 @@ +name: Auto-tag +on: + push: + tags: + - '*.*.*' +jobs: + auto-tag: + name: Auto-tag + runs-on: ubuntu-latest + steps: + - name: Auto-tag + uses: silverstripe/gha-auto-tag@v1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e635dfc --- /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: 8.1 + + - 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..81fdeb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.json +.phpunit.result.cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..78d7c72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022-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 7b8019f..d2c379e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# gha-merge-up -GitHub Action to merge-up supported branches in a repository +# GitHub Actions - Merge-up + +Merge-up supported branches in a repository + +## Usage + +**workflow.yml** +```yml +steps: + - name: Merge-up + uses: silverstripe/gha-merge-up@v1 +``` + +This action has no inputs diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..62f7e2f --- /dev/null +++ b/action.yml @@ -0,0 +1,199 @@ +name: Merge up +description: GitHub Action to merge-up supported branches in a repository + +runs: + using: composite + steps: + + - name: Install PHP + uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2.22.0 + with: + php-version: '8.1' + + - name: Determine if should merge-up + id: determine + shell: bash + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REF_NAME: ${{ github.ref_name }} + run: | + # The minimum cms major with commercial support - configured at a global level + # Change this when major version support changes + MINIMUM_CMS_MAJOR=4 + + # Get the default branch from GitHub API + # We need to make an API call rather than just assume that the current branch is the default + # because this workflow may be triggered by workflow_dispatch on any branch + RESP_CODE=$(curl -w %{http_code} -s -o __base.json \ + -X GET "https://api.github.com/repos/$GITHUB_REPOSITORY" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ) + if [[ $RESP_CODE != "200" ]]; then + echo "Unable to read list of tags - HTTP response code was $RESP_CODE" + exit 1 + fi + + # Gets all tags from GitHub API + # https://docs.github.com/en/rest/git/tags?apiVersion=2022-11-28 + RESP_CODE=$(curl -w %{http_code} -s -o __tags.json \ + -X GET "https://api.github.com/repos/$GITHUB_REPOSITORY/tags?per_page=100" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ) + if [[ $RESP_CODE != "200" ]]; then + echo "Unable to read list of tags - HTTP response code was $RESP_CODE" + exit 1 + fi + + # Gets all branches from GitHub API + # https://docs.github.com/en/rest/branches/branches?apiVersion=2022-11-28#list-branches + RESP_CODE=$(curl -w %{http_code} -s -o __branches.json \ + -X GET "https://api.github.com/repos/$GITHUB_REPOSITORY/branches?per_page=100" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ) + if [[ $RESP_CODE != "200" ]]; then + echo "Unable to read list of tags - HTTP response code was $RESP_CODE" + exit 1 + fi + + DEFAULT_BRANCH=$(jq -r .default_branch __base.json) + echo "DEFAULT_BRANCH is $DEFAULT_BRANCH" + rm __base.json + + if [[ $DEFAULT_BRANCH != $GITHUB_REF_NAME ]]; then + echo "Current branch $GITHUB_REF_NAME is not the same as default branch $DEFAULT_BRANCH" + exit 1 + fi + + # Download composer.json for use in branches.php + curl -s -o __composer.json https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$DEFAULT_BRANCH/composer.json + + BRANCHES=$(MINIMUM_CMS_MAJOR=$MINIMUM_CMS_MAJOR DEFAULT_BRANCH=$DEFAULT_BRANCH php ${{ github.action_path }}/branches.php) + echo "BRANCHES is $BRANCHES" + if [[ $BRANCHES =~ "^FAILURE \- (.+)$" ]]; then + MESSAGE=${BASH_REMATCH[1]} + echo "Exception in branches.php - $MESSAGE" + exit 1 + fi + if [[ $BRANCHES == "" ]]; then + echo "No branches to merge-up" + exit 0 + fi + echo "branches=$BRANCHES" >> $GITHUB_OUTPUT + rm __tags.json + rm __branches.json + rm __composer.json + + # Check to see if there is anything to merge-up + # Technically we could skip these API calls and just do the checking using only git, though + # doing this way not checkout the entire git history of the repo if possible + # These API calls are fast so it really doesn't add much overhead + # Downside to this is that we will abort early and not merge-up anything when we may have been + # able to say merge-up 4.13 -> 4 but not 4 -> 5.0 + FROM_BRANCH="" + INTO_BRANCH="" + for BRANCH in $BRANCHES; do + FROM_BRANCH=$INTO_BRANCH + INTO_BRANCH=$BRANCH + if [[ $FROM_BRANCH == "" ]]; then + continue + fi + # https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits + RESP_CODE=$(curl -w %{http_code} -s -o __compare.json \ + -X GET "https://api.github.com/repos/$GITHUB_REPOSITORY/compare/$INTO_BRANCH...$FROM_BRANCH" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ) + if [[ $RESP_CODE != "200" ]]; then + echo "Unable to compare branches - HTTP response code was $RESP_CODE" + exit 1 + fi + FILES=$(jq -r .files[].filename __compare.json) + rm __compare.json + + # Don't allow merge-ups when there are changes in dependency files + DEPENDENCY_FILES="composer.json package.json yarn.lock" + for DEPENDENCY_FILE in $DEPENDENCY_FILES; do + if [[ $(echo "$FILES" | grep $DEPENDENCY_FILE) != "" ]]; then + echo "Unable to mergeup between $FROM_BRANCH and $INTO_BRANCH - there are changes in $DEPENDENCY_FILE" + exit 1 + fi + done + + # Don't allow merge-ups when there are JS changes that would require a yarn build + if [[ $(echo "$FILES" | grep client/) != "" ]]; then + echo "Unable to mergeup between $FROM_BRANCH and $INTO_BRANCH - there are changes to JS files" + exit 1 + fi + done + + # actions/checkout with fetch-depth: 0 will fetch ALL git history for the repository + # this is required for a merge-up to ensure that nothing is missed + - name: Checkout code + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + with: + fetch-depth: 0 + + - name: Git merge-up + shell: bash + env: + BRANCHES: ${{ steps.determine.outputs.branches }} + run: | + # Set git user to github-actions bot + # The 41898282+ email prefixed is the required, matches the ID here + # https://api.github.com/users/github-actions%5Bbot%5D + # https://github.community/t/github-actions-bot-email-address/17204/6 + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions" + + FROM_BRANCH="" + INTO_BRANCH="" + for BRANCH in $BRANCHES; do + FROM_BRANCH=$INTO_BRANCH + INTO_BRANCH=$BRANCH + if [[ $FROM_BRANCH == "" ]]; then + continue + fi + echo "Attempting to merge-up $FROM_BRANCH into $INTO_BRANCH" + + # Checkout both branches to ensure branch info is up to date + git checkout $FROM_BRANCH + git checkout $INTO_BRANCH + + # Perform the merge-up + git merge --no-ff --no-commit $FROM_BRANCH + + # Check for merge conflicts - this is just an additional check that is probably + # not required as git seems like it does the equivalent of exit 1 when it + # detects a merge conflict. Still it doesn't hurt to be extra cautious. + GIT_STATUS=$(git status) + if [[ "$GIT_STATUS" =~ 'Changes not staged for commit' ]]; then + echo "Merge conflict found when merging-up $FROM_BRANCH into $INTO_BRANCH. Aborting." + exit 1 + fi + + # Check for any random files that shouldn't be committed + if [[ "$GIT_STATUS" =~ 'Untracked files' ]]; then + echo "Untracked files found when merging-up $FROM_BRANCH into $INTO_BRANCH. Aborting." + exit 1 + fi + + # Continue if there's nothing to commit + if [[ "$GIT_STATUS" =~ 'nothing to commit, working tree clean' ]]; then + echo "No changes found when merging-up $FROM_BRANCH into $INTO_BRANCH. Skipping." + continue + fi + + # Commit and push the merge-up + # The commit message matches the one created by git when doing a merge-up + # That only $FROM_BRANCH is quoted and not $INTO_BRANCH is intentional + git commit -m "Merge branch '$FROM_BRANCH' into $INTO_BRANCH" + git push origin $INTO_BRANCH + echo "Succesfully merged-up $FROM_BRANCH into $INTO_BRANCH" + done diff --git a/branches.php b/branches.php new file mode 100644 index 0000000..25b5844 --- /dev/null +++ b/branches.php @@ -0,0 +1,9 @@ +require->{'silverstripe/framework'} ?? ''); + if (preg_match('#^([0-9]+)+\.?[0-9]*$#', $version, $matches)) { + $defaultCmsMajor = $matches[1]; + } else { + $phpVersion = $json->require->{'php'} ?? ''; + if (substr($phpVersion,0, 4) === '^7.4') { + $defaultCmsMajor = 4; + } elseif (substr($phpVersion,0, 4) === '^8.1') { + $defaultCmsMajor = 5; + } + } + if ($defaultCmsMajor === '') { + throw new Exception('Could not work out what the default CMS major version this module uses'); + } + // work out major diff e.g for silverstripe/admin for CMS 5 => 5 - 2 = 3 + $majorDiff = $defaultCmsMajor - $defaultMajor; + + $minorsWithStableTags = []; + $contents = $tagsJson ?: file_get_contents('__tags.json'); + foreach (json_decode($contents) as $row) { + $tag = $row->name; + if (!preg_match('#^([0-9]+)\.([0-9]+)\.([0-9]+)$#', $tag, $matches)) { + continue; + } + $minor = $matches[1] . '.' . $matches[2]; + $minorsWithStableTags[] = $minor; + } + + $branches = []; + $contents = $branchesJson ?: file_get_contents('__branches.json'); + foreach (json_decode($contents) as $row) { + $branch = $row->name; + // filter out non-standard branches + if (!preg_match('#^([0-9]+)+\.?[0-9]*$#', $branch, $matches)) { + continue; + } + // filter out majors that are too old + $major = $matches[1]; + if (($major + $majorDiff) < $minimumCmsMajor) { + continue; + } + // filter out minor branches that are pre-stable + if (preg_match('#^([0-9]+)\.([0-9]+)$#', $branch, $matches)) { + $minor = $matches[1] . '.' . $matches[2]; + if (!in_array($branch, $minorsWithStableTags)) { + continue; + } + } + // suffix a .999 minor version to major branches + if (preg_match('#^[0-9]+$#', $branch)) { + $branch .= '.999'; + } + $branches[] = $branch; + } + + // sort so that newest is first + usort($branches, 'version_compare'); + $branches = array_reverse($branches); + + // remove the .999 + array_walk($branches, function(&$branch) { + $branch = preg_replace('#\.999$#', '', $branch); + }); + + // remove everything except the latest minor from each major line + $foundMinorInMajors = []; + foreach ($branches as $i => $branch) { + if (!preg_match('#^([0-9]+)\.[0-9]+$#', $branch, $matches)) { + continue; + } + $major = $matches[1]; + if (isset($foundMinorInMajors[$major])) { + unset($branches[$i]); + continue; + } + $foundMinorInMajors[$major] = $branch; + } + + // reverse the array so that oldest is first + $branches = array_reverse($branches); + + return $branches; +} 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/tests/BranchesTest.php b/tests/BranchesTest.php new file mode 100644 index 0000000..db5b2ef --- /dev/null +++ b/tests/BranchesTest.php @@ -0,0 +1,88 @@ +assertSame($expected, $actual); + } + + public function provideBranches() + { + return [ + 'Standard' => [ + 'expected' => ['4.13', '4', '5.0', '5'], + 'defaultBranch' => '5', + 'minimumCmsMajor' => '4', + 'composerJson' => << << << [ + 'expected' => ['1.13', '2.0', '2'], + 'defaultBranch' => '2', + 'minimumCmsMajor' => '4', + 'composerJson' => << << <<