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(<hotfixletter>|rc)

A script will be used to clean-up the existing tags.
This commit is contained in:
Andreas Gohr 2023-02-23 15:03:01 +01:00
parent a42c05d2dd
commit 290ea73da0
8 changed files with 439 additions and 105 deletions

View File

@ -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 =

1
.gitattributes vendored
View File

@ -5,6 +5,7 @@
*.ico binary
*.xcf binary
.git export-ignore
.gitattributes export-ignore
.github export-ignore
.gitignore export-ignore

188
.github/release.php vendored Normal file
View File

@ -0,0 +1,188 @@
<?php
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../');
require_once(DOKU_INC . 'vendor/autoload.php');
require_once DOKU_INC . 'inc/load.php';
/**
* Command Line utility to gather and check data for building a release
*/
class Release extends splitbrain\phpcli\CLI
{
// base URL to fetch raw files from the stable branch
protected $BASERAW = 'https://raw.githubusercontent.com/splitbrain/dokuwiki/stable/';
/** @inheritdoc */
public function __construct($autocatch = true)
{
parent::__construct($autocatch);
$this->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();

52
.github/version.php vendored
View File

@ -1,52 +0,0 @@
<?php
/**
* Command line tool to check proper version strings
*
* Expects a tag as first argument. Used in release action to ensure proper formats
* in VERSION file and git tag.
*/
if (!isset($argv[1])) {
echo "::error::No git tag given, this action should not have run\n";
exit(1);
}
$TAG = $argv[1];
$TAG = str_replace('refs/tags/', '', $TAG);
if (!file_exists(__DIR__ . '/../VERSION')) {
echo "::error::No VERSION file found\n";
exit(1);
}
$FILE = trim(file_get_contents(__DIR__ . '/../VERSION'));
$FILE = explode(' ', $FILE)[0];
if(!preg_match('/^release_(stable|candidate)_((\d{4})-(\d{2})-(\d{2})([a-z])?)$/', $TAG, $m)) {
echo "::error::Git tag does not match expected format: $TAG\n";
exit(1);
} else {
$TAGTYPE = $m[1];
$TAGVERSION = $m[2];
}
if(!preg_match('/^(rc)?((\d{4})-(\d{2})-(\d{2})([a-z])?)$/', $FILE, $m)) {
echo "::error::VERSION file does not match expected format: $FILE\n";
exit(1);
} else {
$FILETYPE = $m[1] == 'rc' ? 'candidate' : 'stable';
$FILEVERSION = $m[2];
$TGZVERSION = $m[0];
}
if($TAGTYPE !== $FILETYPE) {
echo "::error::Type of release mismatches between git tag and VERSION file: $TAGTYPE <-> $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";

39
.github/workflows/deletedFiles.yml vendored Normal file
View File

@ -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

106
.github/workflows/release-build.yml vendored Normal file
View File

@ -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 <server>
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"

View File

@ -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.

View File

@ -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 <server>
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"