From 290ea73da0bf2b8df7bddcb78a7c73c52929c96c Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 23 Feb 2023 15:03:01 +0100 Subject: [PATCH] Automate the release workflow using GitHub actions This introduces a two-part release mechanism. A manually triggered workflow asks for the important info like type of release (stable, rc) and code name. It then creates a cleanly mergable pull request. When that pull request is merged, a release is automatically tagged, built and uploaded. Another workflow is introduced to keep track of the deleted.files info. This is one less chore to do on a release. A new scheme for tags is also introduced, making all tags sortable, regardless of their type. They follow the pattern release-YYYY-MM-DD(|rc) A script will be used to clean-up the existing tags. --- .editorconfig | 3 + .gitattributes | 1 + .github/release.php | 188 ++++++++++++++++++++++++++++++ .github/version.php | 52 --------- .github/workflows/deletedFiles.yml | 39 +++++++ .github/workflows/release-build.yml | 106 +++++++++++++++++ .github/workflows/release-preparation.yml | 102 ++++++++++++++++ .github/workflows/release.yml | 53 --------- 8 files changed, 439 insertions(+), 105 deletions(-) create mode 100644 .github/release.php delete mode 100644 .github/version.php create mode 100644 .github/workflows/deletedFiles.yml create mode 100644 .github/workflows/release-build.yml create mode 100644 .github/workflows/release-preparation.yml delete mode 100644 .github/workflows/release.yml diff --git a/.editorconfig b/.editorconfig index d88e75a28..d5465620f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,9 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.{yml,yaml}] +indent_size = 2 + [{vendor,inc/phpseclib}/**] ; Use editor default (possible autodetection). indent_style = diff --git a/.gitattributes b/.gitattributes index 3864eaefc..375ce27be 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ *.ico binary *.xcf binary +.git export-ignore .gitattributes export-ignore .github export-ignore .gitignore export-ignore diff --git a/.github/release.php b/.github/release.php new file mode 100644 index 000000000..e74bc93bc --- /dev/null +++ b/.github/release.php @@ -0,0 +1,188 @@ +error(print_r($_ENV, true)); + + // when running on a clone, use the correct base URL + $repo = getenv('GITHUB_REPOSITORY'); + if ($repo) { + $this->BASERAW = 'https://raw.githubusercontent.com/' . $repo . '/stable/'; + } + } + + + protected function setup(\splitbrain\phpcli\Options $options) + { + $options->setHelp('This tool is used to gather and check data for building a release'); + + $options->registerCommand('new', 'Get environment for creating a new release'); + $options->registerOption('type', 'The type of release to build', null, 'stable|hotfix|rc', 'new'); + $options->registerOption('date', 'The date to use for the version. Defaults to today', null, 'YYYY-MM-DD', 'new'); + $options->registerOption('name', 'The codename to use for the version. Defaults to the last used one', null, 'codename', 'new'); + + $options->registerCommand('current', 'Get environment of the current release'); + } + + protected function main(\splitbrain\phpcli\Options $options) + { + switch ($options->getCmd()) { + case 'new': + $this->prepareNewEnvironment($options); + break; + case 'current': + $this->prepareCurrentEnvironment($options); + break; + default: + echo $options->help(); + } + } + + /** + * Prepare environment for the current branch + */ + protected function prepareCurrentEnvironment(\splitbrain\phpcli\Options $options) + { + $current = $this->getLocalVersion(); + // we name files like the string in the VERSION file, with rc at the front + $current['file'] = ($current['type'] === 'rc' ? 'rc' : '') . $current['date'] . $current['hotfix']; + + // output to be piped into GITHUB_ENV + foreach ($current as $k => $v) { + echo "current_$k=$v\n"; + } + } + + /** + * Prepare environment for creating a new release + */ + protected function prepareNewEnvironment(\splitbrain\phpcli\Options $options) + { + $current = $this->getUpstreamVersion(); + + // continue if we want to create a new release + $next = [ + 'type' => $options->getOpt('type'), + 'date' => $options->getOpt('date'), + 'codename' => $options->getOpt('name'), + 'hotfix' => '', + ]; + if (!$next['type']) $next['type'] = 'stable'; + if (!$next['date']) $next['date'] = date('Y-m-d'); + if (!$next['codename']) $next['codename'] = $current['codename']; + $next['codename'] = ucwords(strtolower($next['codename'])); + + if (!in_array($next['type'], ['stable', 'hotfix', 'rc'])) { + throw new \splitbrain\phpcli\Exception('Invalid release type, use release or rc'); + } + + if ($next['type'] === 'hotfix') { + $next['update'] = floatval($current['update']) + 0.1; + $next['codename'] = $current['codename']; + $next['date'] = $current['date']; + $next['hotfix'] = $this->increaseHotfix($current['hotfix']); + } else { + $next['update'] = intval($current['update']) + 1; + } + + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $next['date'])) { + throw new \splitbrain\phpcli\Exception('Invalid date format, use YYYY-MM-DD'); + } + + if ($current['date'] > $next['date']) { + throw new \splitbrain\phpcli\Exception('Date must be equal or later than the last release'); + } + + if ($current['type'] === 'rc' && $next['type'] === 'hotfix') { + throw new \splitbrain\phpcli\Exception( + 'Cannot create hotfixes for release candidates, create a new RC instead' + ); + } + + if ($current['type'] === 'stable' && $next['type'] !== 'hotfix' && $current['codename'] === $next['codename']) { + throw new \splitbrain\phpcli\Exception('Codename must be different from the last release'); + } + + $next['version'] = $next['date'] . ($next['type'] === 'rc' ? 'rc' : $next['hotfix']); + $next['raw'] = ($next['type'] === 'rc' ? 'rc' : '') . + $next['date'] . + $next['hotfix'] . + ' "' . $next['codename'] . '"'; + + // output to be piped into GITHUB_ENV + foreach ($current as $k => $v) { + echo "current_$k=$v\n"; + } + foreach ($next as $k => $v) { + echo "next_$k=$v\n"; + } + } + + /** + * Get current version info from local VERSION file + * + * @return string[] + */ + protected function getLocalVersion() + { + $versioninfo = \dokuwiki\Info::parseVersionString(trim(file_get_contents('VERSION'))); + $doku = file_get_contents('doku.php'); + if (!preg_match('/\$updateVersion = "(\d+(\.\d+)?)";/', $doku, $m)) { + throw new \Exception('Could not find $updateVersion in doku.php'); + } + $versioninfo['update'] = floatval($m[1]); + return $versioninfo; + } + + /** + * Get current version info from stable branch + * + * @return string[] + * @throws Exception + */ + protected function getUpstreamVersion() + { + // basic version info + $versioninfo = \dokuwiki\Info::parseVersionString(trim(file_get_contents($this->BASERAW . 'VERSION'))); + + // update version grepped from the doku.php file + $doku = file_get_contents($this->BASERAW . 'doku.php'); + if (!preg_match('/\$updateVersion = "(\d+(\.\d+)?)";/', $doku, $m)) { + throw new \Exception('Could not find $updateVersion in doku.php'); + } + $versioninfo['update'] = floatval($m[1]); + + return $versioninfo; + } + + /** + * Increase the hotfix letter + * + * (max 26 hotfixes) + * + * @param string $hotfix + * @return string + */ + protected function increaseHotfix($hotfix) + { + if (empty($hotfix)) return 'a'; + return substr($hotfix, 0, -1) . chr(ord($hotfix) + 1); + } +} + +(new Release())->run(); diff --git a/.github/version.php b/.github/version.php deleted file mode 100644 index c85a81910..000000000 --- a/.github/version.php +++ /dev/null @@ -1,52 +0,0 @@ - $FILETYPE\n"; - exit(1); -} - -if($TAGVERSION !== $FILEVERSION) { - echo "::error::Version date mismatches between git tag and VERSION file: $TAGVERSION <-> $FILEVERSION\n"; - exit(1); -} - -// still here? all good, export Version -echo "::set-output name=VERSION::$TGZVERSION\n"; diff --git a/.github/workflows/deletedFiles.yml b/.github/workflows/deletedFiles.yml new file mode 100644 index 000000000..885c98190 --- /dev/null +++ b/.github/workflows/deletedFiles.yml @@ -0,0 +1,39 @@ +# This workflow updates the list of deleted files based on the recent changes and creates a pull request. +# It compares the current master with the stable branch and adds all deleted files to the data/deleted.files file +# unless they are already listed there or are excluded from the release archives (export-ignore in .gitattributes). + +name: "Update deleted files" +on: + push: + branches: + - master + +jobs: + update: + name: Update deleted files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Update deleted files + run: | + for F in $(git diff origin/stable..HEAD --summary | awk '/^ delete/ && $4 !~ /^(VERSION)/ {print $4}'); do + if grep -q "^$F export-ignore" .gitattributes; then + continue + fi + if grep -q "^$F" data/deleted.files; then + continue + fi + echo "$F" >> data/deleted.files + done + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + commit-message: "Update deleted files" + title: "Update deleted files" + body: "This updates the list of deleted files based on the recent changes." + delete-branch: true diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 000000000..77eeb2916 --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,106 @@ +# This workflow creates a new tag, builds the release archives and uploads them to GitHub and our server +# It is triggered by pushing to the stable branch, either manually or by merging a PR created by the +# release-preparation workflow + +name: "Release: Tag, Build & Deploy" +on: + push: + branches: + - stable + +jobs: + + tag: + name: Tag Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Prepare Environment + run: | + php .github/release.php current >> $GITHUB_ENV + + - name: Check if a tag already exists + run: | + if git rev-parse "release-${{ env.current_version }}" >/dev/null 2>&1; then + echo "::error::Tag already exists, be sure to update the VERSION file for a hotfix" + exit 1 + fi + + - name: Create tag + uses: actions/github-script@v6 + with: + # a privileged token is needed here to create the (protected) tag + github-token: ${{ secrets.RELEASE_TOKEN }} + script: | + const {current_version} = process.env; + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/release-${current_version}`, + sha: context.sha + }); + + build: + name: Build Release + needs: tag + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Prepare Environment + run: | + php .github/release.php current >> $GITHUB_ENV + + - name: Build Archives + run: | + for F in $(awk '/export-ignore/{print $1}' .gitattributes); do + rm -rf $F + done + mkdir -p data/pages/playground + echo "====== PlayGround ======" > data/pages/playground/playground.txt + cd .. + mv ${{ github.event.repository.name }} "dokuwiki-${{ env.current_file }}" + tar -czvf "dokuwiki-${{ env.current_file }}.tgz" dokuwiki-${{ env.current_file }} + zip -r "dokuwiki-${{ env.current_file }}.zip" dokuwiki-${{ env.current_file }} + rm -rf "dokuwiki-${{ env.current_file }}" + mkdir ${{ github.event.repository.name }} + mv "dokuwiki-${{ env.current_version }}.tgz" ${{ github.event.repository.name }}/ + mv "dokuwiki-${{ env.current_version }}.zip" ${{ github.event.repository.name }}/ + + - name: Release to Github + id: release + uses: softprops/action-gh-release@v1 + with: + name: DokuWiki ${{ env.current_raw }} [${{ env.current_update }}] + tag_name: release-${{ env.current_version }} + files: | + dokuwiki-${{ env.current_file }}.tgz + dokuwiki-${{ env.current_file }}.zip + outputs: + version: ${{ env.current_version }} + file: ${{ env.current_file }} + url: ${{ steps.release.outputs.url }} + + deploy: + name: Deploy Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download + run: | + wget ${{ needs.build.outputs.url }}/dokuwiki-${{ needs.build.outputs.file }}.tgz + + - name: Setup SSH Key + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.SSH_PRIVATE_KEY }} + # generate with ssh-keyscan -H + known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} + + - name: Deploy to Server + run: | + scp "dokuwiki-${{ needs.build.outputs.file }}.tgz" ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:htdocs/src/dokuwiki/ + ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd htdocs/src/dokuwiki/ && tar -xzvf dokuwiki-${{ needs.build.outputs.file }}.tgz" diff --git a/.github/workflows/release-preparation.yml b/.github/workflows/release-preparation.yml new file mode 100644 index 000000000..703b98458 --- /dev/null +++ b/.github/workflows/release-preparation.yml @@ -0,0 +1,102 @@ +# This workflow is triggered manually and prepares a new release by creating a pull request +# All needed info is provided by the user in the workflow_dispatch dialog +# +# When the pull request is merged, the release-build workflow will be triggered automatically + +name: "Release: Preparation 🚀" +on: + workflow_dispatch: + inputs: + type: + description: 'What type of release is this?' + required: true + default: 'stable' + type: choice + options: + - stable + - hotfix + - rc + codename: + description: 'The codename for this release, empty for same as last' + required: false + version: + description: 'The version date YYYY-MM-DD, empty for today' + required: false + +jobs: + create: + name: Prepare Pull Request + runs-on: ubuntu-latest + steps: + - name: Fail if branch is not master + if: github.ref != 'refs/heads/master' + run: | + echo "::error::This workflow should only be triggered on master" + exit 1 + + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set git identity + run: | + git config --global user.name "${{ github.actor }}" + git config --global user.email "${{ github.actor }}@users.noreply.github.com" + + - name: Prepare Environment + run: | + php .github/release.php new \ + --date "${{ inputs.version }}" \ + --name "${{ inputs.codename }}" \ + --type "${{ inputs.type }}" \ + >> $GITHUB_ENV + + - name: Check if a tag of the new release already exists + run: | + if git rev-parse "release-${{ env.next_version }}" >/dev/null 2>&1; then + echo "::error::Tag already exists, you may need to build a hotfix instead" + exit 1 + fi + + - name: Create merge commit with version info + run: | + git merge -s ours origin/stable + echo '${{ env.next_raw }}' > VERSION + git add VERSION + git commit --amend -m 'Release preparations for ${{ env.next_raw }}' + git log -1 + git log origin/stable..master --oneline + git checkout -B auto-${{ env.next_version }} + git push --set-upstream origin auto-${{ env.next_version }} + + - name: Create pull request + uses: repo-sync/pull-request@v2 + with: + source_branch: auto-${{ env.next_version }} + destination_branch: stable + pr_title: Release Preparations for ${{ env.next_raw }} + pr_body: | + With accepting this PR, a the stable branch will be updated and the whole release and + deployment process will be triggered. + + If you're not happy with the contents of this PR, please close it, fix stuff and trigger + the workflow again. + + * ${{ env.current_raw }} -> ${{ env.next_raw }} + * Update Version ${{ env.current_update }} -> ${{ env.next_update }} + + Before merging this PR, make sure that: + + - [ ] Ensure all tests pass + - [ ] If this is a new stable release, make sure you merged `stable` into `old-stable` first + - [ ] Check that a meaningful [changelog](https://www.dokuwiki.org/changes) exists + + After merging, the release workflow will be triggered automatically. + + After this is done, you need to do the following things manually: + + - [ ] Update the [version symlinks](https://download.dokuwiki.org/admin/) + - [ ] Update the update message system + - [ ] Announce the release on the mailing list, forum, IRC, social media, etc. + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 7c86c2835..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Build and Publish -on: - push: - tags: - - '*' - -permissions: - contents: read # to fetch code (actions/checkout) - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 7.4 - - - name: Get the version - id: get_version - run: php .github/version.php "${GITHUB_REF}" - - - name: Build TGZ - run: | - rm -rf .gitignore - rm -rf .git - rm -rf .github - rm -rf .gitattributes - rm -rf _test - rm -f .editorconfig - mkdir -p data/pages/playground - echo "====== PlayGround ======" > data/pages/playground/playground.txt - cd .. - mv dokuwiki "dokuwiki-${{ steps.get_version.outputs.VERSION }}" - tar -czvf "dokuwiki-${{ steps.get_version.outputs.VERSION }}.tgz" dokuwiki-${{ steps.get_version.outputs.VERSION }} - rm -rf "dokuwiki-${{ steps.get_version.outputs.VERSION }}" - mkdir dokuwiki - mv "dokuwiki-${{ steps.get_version.outputs.VERSION }}.tgz" dokuwiki/ - - - name: Setup SSH Key - uses: shimataro/ssh-key-action@v2 - with: - key: ${{ secrets.SSH_PRIVATE_KEY }} - # generate with ssh-keyscan -H - known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} - - - name: Deploy to Server - run: | - scp "dokuwiki-${{ steps.get_version.outputs.VERSION }}.tgz" ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:htdocs/src/dokuwiki/ - ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd htdocs/src/dokuwiki/ && tar -xzvf dokuwiki-${{ steps.get_version.outputs.VERSION }}.tgz" -- 2.11.4.GIT