diff --git a/demo/bin/pma-demo-update b/demo/bin/pma-demo-update
index c0fb45e..0d5694f 100755
--- a/demo/bin/pma-demo-update
+++ b/demo/bin/pma-demo-update
@@ -1,117 +1,308 @@
-#!/bin/sh
+#!/bin/bash
+
 # Script to update repositories for the demo server
-# Requires ~/scripts to be checkout of scripts repository
+# Requires $HOME/scripts to be a checkout of the scripts repository
 
+# Fail on undefined variables
+set -u
+# Fail on failure
 set -e
+# If a script fails, stop before the next pipe
+set -o pipefail
+
+FORCE_MODE="0"
+QUIET_MODE="0"
+OFFLINE_MODE="0"
+LOG_FILE="/dev/null"
+
+# Source: https://stackoverflow.com/a/31024664/5155484
+while [[ $# -gt 0 ]]
+do
+    key="$1"
+    while [[ ${key+x} ]]
+    do
+        case $key in
+            --offline)
+                OFFLINE_MODE="1"
+                ;;
+            --force)
+                FORCE_MODE="1"
+                ;;
+            -q)
+                QUIET_MODE="1"
+                ;;
+            --log-file)
+                LOG_FILE="$2"
+                shift # option has parameter
+                ;;
+            *)
+                # unknown option
+                echo "Unknown option: $key" #1>&2
+                exit 10
+                ;;
+        esac
+        # prepare for next option in this key, if any
+        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
+    done
+    shift # option(s) fully processed, proceed to next input argument
+done
+
+debugLog() {
+    echo "$1" >> "$LOG_FILE"
+    if [ "${QUIET_MODE}" = "1" ] ; then
+        return
+    fi
+    echo "$1"
+}
+
+SCRIPTS_FOLDER="${SCRIPTS_FOLDER:-$HOME/scripts}"
 
 # Directory where repositories are stored
-BASEDIR=~/.pma-demo
+GIT_STORAGE="${GIT_STORAGE:-$HOME/.pma-demo}"
+
+# Directory where built folders are stored
+WEB_STORAGE="${WEB_STORAGE:-$HOME/.pma-demo-web}"
+# The user group to own the files
+WEB_GROUP="${WEB_GROUP:-www-data}"
 
 # Remote URL of repository to checkout
-REMOTE=https://github.com/phpmyadmin/phpmyadmin.git
-REMOTE_THEMES=https://github.com/phpmyadmin/themes.git
+REMOTE="${REMOTE:-https://github.com/phpmyadmin/phpmyadmin.git}"
+REMOTE_THEMES="${REMOTE_THEMES:-https://github.com/phpmyadmin/themes.git}"
 
 # List of repositories to checkout
-REPOS=`awk '/^branches/ {print $3}' ~/scripts/demo/php/versions.ini`
-THEMES=`awk '/^themes/ {print $3}' ~/scripts/demo/php/versions.ini`
-
-# Update all repositories
-for REPONAME in $REPOS ; do
-    DIRNAME=$BASEDIR/$REPONAME
-    mkdir -p $DIRNAME
-
-    if [ ! -d $DIRNAME/.git ] ; then
-        cd $BASEDIR
-
-        # Clone the repository
-        if [ $REPONAME != 'master' ] ; then
-            # Use master as reference repository to share objects database
-            git clone --reference $BASEDIR/master $REMOTE $REPONAME
-        else
-            git clone $REMOTE $REPONAME
+VARIANTS=$(awk '/^variants/ {print $3}' "$SCRIPTS_FOLDER/demo/php/versions.ini")
+THEMES=$(awk '/^themes/ {print $3}' "$SCRIPTS_FOLDER/demo/php/versions.ini")
+
+debugLog "Updating themes"
+
+# Clone / update themes
+if [ -d "$GIT_STORAGE/themes/.git" ] ; then
+    cd "$GIT_STORAGE/themes"
+    # Remove local theme changes
+    git reset --hard -q
+    if [ $OFFLINE_MODE = 0 ] ; then
+        git pull -q
+    fi
+else
+    git clone "$REMOTE_THEMES" "$GIT_STORAGE/themes"
+fi
+
+# Clone / update phpMyAdmin
+if [ -d "$GIT_STORAGE/phpMyAdmin/.git" ] ; then
+    cd "$GIT_STORAGE/phpMyAdmin"
+    # Remove local changes
+    git reset --hard -q
+    if [ $OFFLINE_MODE = 0 ] ; then
+        git fetch --all -p
+    fi
+    git checkout temp 2> /dev/null 1> /dev/null
+else
+    git clone "$REMOTE" "$GIT_STORAGE/phpMyAdmin"
+    git checkout -b temp 2> /dev/null 1> /dev/null
+fi
+
+debugLog "Deleting stale worktrees"
+cd "$GIT_STORAGE/phpMyAdmin"
+
+WORKTREES="$(git worktree list --porcelain | grep -F "worktree" | cut -d ' ' -f 2)"
+
+# Delete stale worktrees
+for WORKTREE_PATH in $WORKTREES ; do
+    if [ "$WORKTREE_PATH" = "$GIT_STORAGE/phpMyAdmin" ] ; then
+        # Skip the current path
+        continue
+    fi
+
+    debugLog "Checking worktree $WORKTREE_PATH"
+    WORKTREE_NAME=${WORKTREE_PATH##*/}
+    for VARIANT_DATA in $VARIANTS ; do
+        VARIANT_NAME=${VARIANT_DATA%%:*}
+        if [ -z "$VARIANT_NAME" ] ; then
+            debugLog "Invalid variant data: $VARIANT_DATA"
+            continue
+        fi
+
+        if [ "$VARIANT_NAME" = "$WORKTREE_NAME" ] ; then
+            # Skip the item
+            continue 2
         fi
+    done
+    debugLog "Deleting stale worktree $WORKTREE_PATH"
+    if [ -d "$WORKTREE_PATH/.git" ] ; then
+        git worktree remove "$WORKTREE_PATH"
+    else
+        rm -r "$WORKTREE_PATH"
+    fi
+done
+
+unset WORKTREES
+cd - > /dev/null
 
-        # Checkout correct branch
-        cd $DIRNAME
-        git checkout ${REPONAME%%-*}
+# Build worktrees
+for VARIANT_DATA in $VARIANTS ; do
+    VARIANT_NAME=${VARIANT_DATA%%:*}
+    VARIANT_BRANCH=${VARIANT_DATA##*:}
+    if [ -z "$VARIANT_NAME" ] || [ -z "$VARIANT_BRANCH" ] ; then
+        debugLog "Invalid variant data: $VARIANT_DATA"
+        continue
+    fi
+
+    # Only one worktree per branch is possible
+    WORKTREE_PATH="$GIT_STORAGE/$VARIANT_BRANCH"
 
-        # Symlink configuration
-        FLAVOUR=${REPONAME#*-}
-        if [ -f ~/scripts/demo/php/config-$FLAVOUR.inc.php ] ; then
-            ln -sf ~/scripts/demo/php/config-$FLAVOUR.inc.php config.inc.php
-        else
-            ln -sf ~/scripts/demo/php/config.inc.php config.inc.php
+    if [ -d "$WORKTREE_PATH" ] ; then
+        debugLog "Updating worktree $VARIANT_BRANCH for $VARIANT_NAME at $WORKTREE_PATH"
+        cd "$WORKTREE_PATH"
+        # Remove local changes
+        git reset --hard -q
+        if [ $OFFLINE_MODE = 0 ] ; then
+            git pull -q
         fi
-        ln -sf ~/scripts/demo/php/config.*.inc.php .
+        cd - > /dev/null
     else
-        # Check for updates
-        cd $DIRNAME
-        orig_head=$(git log -n 1 --format=format:'%H')
-        git pull -q
-        new_head=$(git log -n 1 --format=format:'%H')
+        debugLog "Creating worktree $VARIANT_BRANCH for $VARIANT_NAME at $WORKTREE_PATH"
+        git worktree add "$WORKTREE_PATH" "$VARIANT_BRANCH"
+    fi
+done
+
+# Cleanup worktree folders
+for VARIANT_DATA in $VARIANTS ; do
+    VARIANT_NAME=${VARIANT_DATA%%:*}
+    VARIANT_BRANCH=${VARIANT_DATA##*:}
+    if [ -z "$VARIANT_NAME" ] || [ -z "$VARIANT_BRANCH" ] ; then
+        debugLog "Invalid variant data: $VARIANT_DATA"
+        continue
+    fi
+
+    WORKTREE_PATH="$GIT_STORAGE/$VARIANT_BRANCH"
+
+    #if [ -d "$WORKTREE_PATH/release" ] ; then
+        #debugLog "Cleaning old release files"
+        #rm -rf "$WORKTREE_PATH/release"
+    #fi
+done
+
+# Build web folders
+for VARIANT_DATA in $VARIANTS ; do
+    VARIANT_NAME=${VARIANT_DATA%%:*}
+    VARIANT_BRANCH=${VARIANT_DATA##*:}
+    if [ -z "$VARIANT_NAME" ] || [ -z "$VARIANT_BRANCH" ] ; then
+        debugLog "Invalid variant data: $VARIANT_DATA"
+        continue
+    fi
 
-        # Check if there has been an update
-        if [ $orig_head = $new_head -a "x$1" != "x--force" ] ; then
+    WORKTREE_PATH="$GIT_STORAGE/$VARIANT_BRANCH"
+
+    debugLog "Building from $WORKTREE_PATH"
+    cd "$WORKTREE_PATH"
+    VERSION="$(grep -F "version" package.json | cut -d '"' -f 4)"
+    debugLog "Building version $VERSION from branch $VARIANT_BRANCH"
+
+    SKIP_BUILD=0
+    if [ -f "$WORKTREE_PATH/release/phpMyAdmin-$VERSION.json" ] ; then
+        BUILT_COMMIT="$(grep -F "commit" "$WORKTREE_PATH/release/phpMyAdmin-$VERSION.json" |cut -d '"' -f 4)"
+        debugLog "Already built at commit $BUILT_COMMIT"
+
+        CURRENT_COMMIT=$(git log -n 1 --format=format:'%H')
+        if [ "$CURRENT_COMMIT" = "$BUILT_COMMIT" ] && [ $FORCE_MODE = 0 ] ; then
+            debugLog "Skipping building version $VERSION from branch $VARIANT_BRANCH"
+            SKIP_BUILD=1
             continue
         fi
     fi
 
-    # Store revision info
-    ~/scripts/demo/bin/pma-revision-info
+    RELEASE_SCRIPT="$WORKTREE_PATH/scripts/create-release.sh"
+    THEMES_FOLDER="themes"
+    if [ -f "$WORKTREE_PATH/bin/internal/create-release.sh" ] ; then
+        RELEASE_SCRIPT="$WORKTREE_PATH/bin/internal/create-release.sh"
+        THEMES_FOLDER="public/themes"
+    fi
 
-    # Create temp dir
-    mkdir -p tmp/
-    chmod 770 tmp/
-    chgrp www-data tmp/
+    if [ $SKIP_BUILD = 0 ] ; then
+        SCRIPT_OPTIONS="--daily --no-sign --revision-info --compressions tgz --kits all-languages"
+        if [ "$VARIANT_BRANCH" = "STABLE" ] ; then
+            # Remove this when 5.2.2 is out
+            SCRIPT_OPTIONS="--daily"
+            debugLog "Skipping, the build is broken anyway"
+            continue
+        fi
+        debugLog "Building the release"
+        COMPOSER_DISABLE_NETWORK="$OFFLINE_MODE" "$RELEASE_SCRIPT" "$VERSION" "$VARIANT_BRANCH" $SCRIPT_OPTIONS 2> $LOG_FILE 1> $LOG_FILE
+    fi
 
-    # Update composer deps
-    if [ -f composer.json ] ; then
-        composer update --quiet --no-dev
+    TARBALL_TO_INSTALL="$WORKTREE_PATH/release/phpMyAdmin-$VERSION-all-languages.tar.gz"
+    if [ ! -f "$TARBALL_TO_INSTALL" ] ; then
+        debugLog "Unable to find $TARBALL_TO_INSTALL"
+        continue
     fi
 
-    if [ -f package.json ] ; then
-        # We still redirect output to /dev/null because even with the --silent flag, yarn still gives sass output such as
-        # $ sass themes/pmahomme/scss:themes/pmahomme/css themes/original/scss:themes/original/css themes/metro/scss:themes/metro/css
-        yarn install --non-interactive --silent > /dev/null
+    WEB_FOLDER="$WEB_STORAGE/$VARIANT_NAME"
+    if [ -d "$WEB_FOLDER" ] ; then
+        debugLog "Cleaning up $WEB_FOLDER"
+        rm -rf "$WEB_FOLDER"
     fi
 
+    debugLog "Creating $WEB_FOLDER"
+    mkdir "$WEB_FOLDER"
+    chgrp "$WEB_GROUP" "$WEB_FOLDER"
+
+    debugLog "Extracting $TARBALL_TO_INSTALL to $WEB_FOLDER"
+    tar -xz --strip-components=1 --directory="${WEB_FOLDER}" -f "${TARBALL_TO_INSTALL}"
+
+    # Symlink configuration
+    FLAVOUR=${VARIANT_NAME#*-}
+    debugLog "Flavour $FLAVOUR, copying configs"
+    if [ -f "$SCRIPTS_FOLDER/demo/php/config-$FLAVOUR.inc.php" ] ; then
+        cp "$SCRIPTS_FOLDER/demo/php/config-$FLAVOUR.inc.php" "$WEB_FOLDER/config.inc.php"
+    else
+        cp "$SCRIPTS_FOLDER/demo/php/config.inc.php" "$WEB_FOLDER/config.inc.php"
+    fi
+    cp "$SCRIPTS_FOLDER/demo/php/"config.*.inc.php "$WEB_FOLDER/"
+
     # Link themes
     for THEME in $THEMES ; do
-        if [ ! -e themes/$THEME ] ; then
-            ln -s ../../themes/$THEME themes/$THEME
+        if [ ! -e "$WEB_FOLDER/$THEMES_FOLDER/$THEME" ] ; then
+            debugLog "Copying theme: $THEME"
+            cp -rp "$GIT_STORAGE/themes/$THEME" "$WEB_FOLDER/$THEMES_FOLDER/$THEME"
         fi
     done
 
-    # Generate locales
-    if [ -f ./scripts/generate-mo ] ; then
-        ./scripts/generate-mo --quiet
-    fi
+    # Apply permissions globally
+    chgrp -R "$WEB_GROUP" "$WEB_FOLDER"
 
-    # Calculate locales percentages to get warnings for missing translations
-    if [ -f ./scripts/remove-incomplete-mo ] ; then
-        ./scripts/remove-incomplete-mo 0
-    fi
+    # Apply permissions to files and folders
+    find "$WEB_FOLDER" -type d -exec chmod 0755 {} \;
+    find "$WEB_FOLDER" -type f -exec chmod 0644 {} \;
+
+    # Create temp dir
+    mkdir -p "$WEB_FOLDER/tmp/"
+    chmod 770 "$WEB_FOLDER/tmp/"
+    chgrp "$WEB_GROUP" "$WEB_FOLDER/tmp/"
 done
 
-# Clone / update themes
-if [ -d $BASEDIR/themes/.git ] ; then
-    cd $BASEDIR/themes
-    git pull -q
-else
-    git clone $REMOTE_THEMES $BASEDIR/themes
-fi
+debugLog "Deleting stale directories"
 
 # Delete stale directories
-find $BASEDIR -maxdepth 1 -mindepth 1 -type d | while read DIRNAME ; do
-    REPO=${DIRNAME##*/}
-    if [ $REPO = themes ] ; then
-        continue
-    fi
-    for TESTREPO in $REPOS ; do
-        if [ $TESTREPO = $REPO ] ; then
+find "$WEB_STORAGE" -maxdepth 1 -mindepth 1 -type d | while read DIRNAME ; do
+    FOLDER_NAME=${DIRNAME##*/}
+
+    # Build web folders
+    for VARIANT_DATA in $VARIANTS ; do
+        VARIANT_NAME=${VARIANT_DATA%%:*}
+        VARIANT_BRANCH=${VARIANT_DATA##*:}
+        if [ -z "$VARIANT_NAME" ] || [ -z "$VARIANT_BRANCH" ] ; then
+            debugLog "Invalid variant data: $VARIANT_DATA"
+            continue
+        fi
+
+        if [ "$FOLDER_NAME" = "$VARIANT_NAME" ] ; then
+            debugLog "Known variant: $FOLDER_NAME"
             continue 2
         fi
     done
-    echo "Deleting stale repository $DIRNAME"
-    rm -rf "$DIRNAME"
+
+    debugLog "Deleting stale variant $FOLDER_NAME"
+    #rm -rf "$DIRNAME"
 done
+
+debugLog "All done"
diff --git a/demo/bin/pma-revision-info b/demo/bin/pma-revision-info
index 8308d09..34cfde6 100755
--- a/demo/bin/pma-revision-info
+++ b/demo/bin/pma-revision-info
@@ -26,5 +26,11 @@ cat > revision-info.php.tmp <<EOT
 \$reponame = '$reponame';
 \$repobranchbase = '$repobranchbase';
 \$branch = '$ref';
+return array(
+    'revision' => '$rev',
+    'revisionUrl' => '$repobase$fullrev',
+    'branch' => '$ref',
+    'branchUrl' => '$repobranchbase$ref'
+);
 EOT
 mv revision-info.php.tmp revision-info.php
diff --git a/demo/php/config-common.inc.php b/demo/php/config-common.inc.php
index e7a9e83..a1c0a20 100644
--- a/demo/php/config-common.inc.php
+++ b/demo/php/config-common.inc.php
@@ -20,7 +20,7 @@
 $cfg['Servers'][$i]['history'] = 'pma__history';
 $cfg['Servers'][$i]['tracking'] = 'pma__tracking';
 $cfg['Servers'][$i]['designer_coords'] = 'pma__designer_coords';
-#$cfg['Servers'][$i]['userconfig'] = 'pma__userconfig';
+// $cfg['Servers'][$i]['userconfig'] = 'pma__userconfig';
 $cfg['Servers'][$i]['recent'] = 'pma__recent';
 $cfg['Servers'][$i]['favorite'] = 'pma__favorite';
 $cfg['Servers'][$i]['users'] = 'pma__users';
@@ -54,7 +54,7 @@
 $cfg['Servers'][$i]['table_uiprefs'] = 'pma__table_uiprefs';
 $cfg['Servers'][$i]['tracking'] = 'pma__tracking';
 $cfg['Servers'][$i]['designer_coords'] = 'pma__designer_coords';
-#$cfg['Servers'][$i]['userconfig'] = 'pma__userconfig';
+// $cfg['Servers'][$i]['userconfig'] = 'pma__userconfig';
 $cfg['Servers'][$i]['recent'] = 'pma__recent';
 $cfg['Servers'][$i]['favorite'] = 'pma__favorite';
 $cfg['Servers'][$i]['users'] = 'pma__users';
@@ -78,7 +78,8 @@
 
 /* End of servers configuration */
 
-$cfg['blowfish_secret'] = '1234567890123456789012345678901234567890';
+// Random generated string (should be 32 bytes long)
+$cfg['blowfish_secret'] = 'nH5&L56HLDFbsgHHpP$HChD8hop@4xHn';
 
 $cfg['Error_Handler']['display'] = true;
 $cfg['Error_Handler']['gather'] = false;
@@ -88,6 +89,5 @@
 $cfg['TranslationWarningThreshold'] = 95;
 $cfg['ServerDefault'] = 2;
 $cfg['CSPAllow'] = 'www.google-analytics.com ssl.google-analytics.com';
-#$cfg['MaxDbList'] = 3;
+// $cfg['MaxDbList'] = 3;
 $cfg['DBG']['demo'] = true;
-?>
diff --git a/demo/php/versions.ini b/demo/php/versions.ini
index 9dea969..0e0eb24 100644
--- a/demo/php/versions.ini
+++ b/demo/php/versions.ini
@@ -1,24 +1,26 @@
 #
 # The demo server configuration
 #
-# It will get updated within one hour from commiting changes.
+# It will get updated within one hour from committing changes.
 
 [demo]
-# List of branches to show, master must come first.
-# The special cases like master-* have just different configuration.
-branches[] = master
-branches[] = master-config
-branches[] = master-http
-branches[] = master-config-nopmadb
-branches[] = STABLE
-branches[] = QA_4_9
-branches[] = QA_5_0
+# Format: <variant-name>:<branch>
+variants[] = master:master
+variants[] = master-config:master
+variants[] = master-http:master
+variants[] = master-config-nopmadb:master
+variants[] = STABLE:STABLE
+variants[] = QA_5_2:QA_5_2
 
-# Version of future release from master branch
-master-release = 5.1.0
+# Version of future release from master branch, used by the website
+master-release = 6.0.0
 
 # List of themes to link
-themes[] = metro
+# mhn is 4.{8,9} only
 themes[] = mhn
-themes[] = pmaterial
 themes[] = fallen
+themes[] = darkmod-neo
+themes[] = darkwolf
+themes[] = boodark
+themes[] = eyed
+themes[] = blueberry
diff --git a/develdocs/build.sh b/develdocs/build.sh
index 7fc6662..1281e62 100755
--- a/develdocs/build.sh
+++ b/develdocs/build.sh
@@ -3,9 +3,12 @@
 # Script to generate phpdoc documentation of phpMyAdmin and publish it
 # on develdocs.phpmyadmin.net
 
+set -e
+
 # Update scripts
-BUILDER_ROOT="/home/builder"
-cd "$BUILDER_ROOT/scripts/"
+BUILDER_ROOT=${BUILDER_ROOT:-"/home/builder"}
+BUILDER_REPO="$BUILDER_ROOT/${BUILDER_REPO_NAME:-scripts}"
+cd "$BUILDER_REPO"
 git pull -q
 
 # Update doc build environment
@@ -17,16 +20,18 @@ for repo in phpmyadmin sql-parser motranslator shapefile simple-math ; do
     cd "$BUILDER_ROOT/$repo"
     git pull -q
     # Clean output
-    rm -rf "$BUILDER_ROOT/scripts/develdocs/output/$repo/"
+    rm -rf "$BUILDER_REPO/develdocs/output/$repo/"
     # Generate config file
-    nice -19 "$BUILDER_ROOT/scripts/develdocs/sami.php" \
+    nice -19 "$BUILDER_REPO/develdocs/doctum.php" \
     --root "$BUILDER_ROOT/$repo" \
-    --build-dir "$BUILDER_ROOT/scripts/develdocs/output/$repo/" \
-    --cache-dir "$BUILDER_ROOT/scripts/develdocs/tmp/$repo/" \
-    --output-config "$BUILDER_ROOT/scripts/develdocs/sami-$repo.php" \
-    --title-of-composer
+    --build-dir "$BUILDER_REPO/develdocs/output/$repo/" \
+    --cache-dir "$BUILDER_REPO/develdocs/tmp/$repo/" \
+    --docs-branch "master" \
+    --slug "phpmyadmin/$repo" \
+    --base-url "https://develdocs.phpmyadmin.net/$repo/" \
+    --output-config "$BUILDER_REPO/develdocs/doctum-$repo.php"
     # Render
-    nice -19 "$BUILDER_ROOT/scripts/develdocs/vendor/bin/sami.php" update  "$BUILDER_ROOT/scripts/develdocs/sami-$repo.php"
+    nice -19 "$BUILDER_REPO/develdocs/vendor/bin/doctum.php" update --force --no-progress --no-ansi --ignore-parse-errors "$BUILDER_REPO/develdocs/doctum-$repo.php"
     # Delete config file
-    rm "$BUILDER_ROOT/scripts/develdocs/sami-$repo.php"
+    rm "$BUILDER_REPO/develdocs/doctum-$repo.php"
 done
diff --git a/develdocs/composer.json b/develdocs/composer.json
index 229df2f..d283320 100644
--- a/develdocs/composer.json
+++ b/develdocs/composer.json
@@ -1,5 +1,5 @@
 {
     "require": {
-        "sami/sami": "^4.0"
+        "code-lts/doctum": "^5.5"
     }
 }
diff --git a/develdocs/doctum.php b/develdocs/doctum.php
new file mode 100755
index 0000000..b491cc0
--- /dev/null
+++ b/develdocs/doctum.php
@@ -0,0 +1,51 @@
+#!/usr/bin/php
+<?php
+$_CLI = getopt('', ['slug:', 'root:', 'base-url:', 'build-dir:', 'cache-dir:', 'docs-branch:', 'output-config:', 'title::', 'title-of-composer']);
+
+$SOURCE = 'src';
+
+if (is_dir($_CLI['root'] . '/src') === false) {
+    $SOURCE = 'libraries';
+}
+
+$output = <<<'EOT'
+<?php
+/**
+ * This file has been generated by phpmyadmin/scripts/develdocs/build.sh
+ * @see https://github.com/phpmyadmin/scripts/blob/master/develdocs/doctum.php
+ * @see https://github.com/phpmyadmin/scripts/blob/master/develdocs/build.sh
+ */
+use Doctum\Doctum;
+use Symfony\Component\Finder\Finder;
+use Doctum\RemoteRepository\GitHubRemoteRepository;
+
+$iterator = Finder::create()
+    ->files()
+    ->name('*.php')
+    ->in('%s');
+
+return new Doctum($iterator, [
+    'title'                => json_decode(file_get_contents('%s'))->description,
+    'build_dir'            => '%s',
+    'cache_dir'            => '%s',
+    'version'              => '%s',
+    'remote_repository'    => new GitHubRemoteRepository('%s', '%s'),
+    'base_url'             => '%s',
+]);
+EOT;
+
+$root = rtrim($_CLI['root'], '/') . '/';
+
+$output = sprintf(
+    $output,
+    $root . $SOURCE,
+    $root . 'composer.json',
+    $_CLI['build-dir'],
+    $_CLI['cache-dir'],
+    $_CLI['docs-branch'],
+    $_CLI['slug'],
+    $root,
+    $_CLI['base-url']
+);
+
+file_put_contents($_CLI['output-config'], $output);
diff --git a/develdocs/output/index.html b/develdocs/output/index.html
index 8ffa34d..8cab315 100644
--- a/develdocs/output/index.html
+++ b/develdocs/output/index.html
@@ -1,14 +1,85 @@
-<html>
-<head><title>phpMyAdmin developer documentation</title></head>
+<!doctype html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="description" content="Auto-generated phpMyAdmin developer documentation">
+    <meta name="author" content="phpMyAdmin team and contributors">
+    <title>phpMyAdmin developer documentation</title>
+
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css"
+        integrity="sha512-rO2SXEKBSICa/AfyhEK5ZqWFCOok1rcgPYfGOqtX35OyiraBg6Xa4NnBJwXgpIRoXeWjcAmcQniMhp22htDc6g=="
+        crossorigin="anonymous" />
+
+</head>
+
 <body>
-<h1>phpMyAdmin developer documentation</h2>
-
-<ul>
-<li><a href="/phpmyadmin/">phpMyAdmin itself</a></li>
-<li><a href="/motranslator/">motranslator</a></li>
-<li><a href="/shapefile/">shapefile</a></li>
-<li><a href="/sql-parser/">sql-parser</a></li>
-<li><a href="/simple-math/">simple-math</a></li>
-</ul>
+    <header>
+        <div class="navbar navbar-dark bg-dark shadow-sm">
+            <div class="container d-flex justify-content-between">
+                <div class="card-footer"></div>
+            </div>
+        </div>
+    </header>
+
+    <main role="main">
+
+        <section class="jumbotron text-center">
+            <div class="container">
+                <h1>phpMyAdmin developer documentation</h1>
+                <p class="lead text-muted">You will find auto-generated documentations for the projects below.</p>
+            </div>
+        </section>
+        <div class="col-12">
+            <div class="card-group">
+                <div class="col-1">&nbsp;</div>
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">phpMyAdmin</h5>
+                        <p class="card-text">A web interface for MySQL and MariaDB</p>
+                    </div>
+                    <div class="card-footer text-center"><a href="/phpmyadmin/" class="card-link">Documentation</a>
+                    </div>
+                </div>
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">motranslator</h5>
+                        <p class="card-text">Translation API for PHP using Gettext MO
+                            files</p>
+                    </div>
+                    <div class="card-footer text-center"><a href="/motranslator/" class="card-link">Documentation</a>
+                    </div>
+                </div>
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">shapefile</h5>
+                        <p class="card-text">ESRI ShapeFile library for PHP</p>
+                    </div>
+                    <div class="card-footer text-center"><a href="/shapefile/" class="card-link">Documentation</a></div>
+                </div>
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">sql-parser</h5>
+                        <p class="card-text">A validating SQL lexer and parser with a
+                            focus on MySQL dialect
+                        </p>
+                    </div>
+                    <div class="card-footer text-center"><a href="/sql-parser/" class="card-link">Documentation</a>
+                    </div>
+                </div>
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">simple-math</h5>
+                        <p class="card-text">Simple math evaluator</p>
+                    </div>
+                    <div class="card-footer text-center"><a href="/simple-math/" class="card-link">Documentation</a>
+                    </div>
+                </div>
+                <div class="col-1">&nbsp;</div>
+            </div>
+        </div>
+    </main>
 </body>
-</html>
+
+</html>
\ No newline at end of file
diff --git a/develdocs/sami.php b/develdocs/sami.php
deleted file mode 100755
index 933c47f..0000000
--- a/develdocs/sami.php
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/php
-<?php
-$_CLI = getopt("", array("root:", "build-dir:", "cache-dir:", "output-config:", "title::", "title-of-composer"));
-
-$SOURCE = 'src';
-
-if (is_dir($_CLI['root'].'/src') == false) {
-    $SOURCE = 'libraries';
-}
-
-if (isset($_CLI['title-of-composer'])) {
-    $contents = file_get_contents($_CLI['root'].'/composer.json');
-    $_CLI['title'] = json_decode($contents)->description;
-}
-
-$output = '<?php
-/**
- * This file has been generated by phpmyadmin/scripts:/develdocs/build.sh
- * @see https://github.com/phpmyadmin/scripts/blob/master/develdocs/sami.php
- * @see https://github.com/phpmyadmin/scripts/blob/master/develdocs/build.sh
- */
-use Sami\Sami;
-use Symfony\Component\Finder\Finder;
-
-$iterator = Finder::create()
-    ->files()
-    ->name("*.php")
-    ->in("'.$_CLI['root'].'/'.$SOURCE.'")
-;
-
-return new Sami($iterator, [
-    "title"                => "'.$_CLI["title"].'",
-    "build_dir"            => "'.$_CLI["build-dir"].'",
-    "cache_dir"            => "'.$_CLI["cache-dir"].'"
-]);
-';
-
-file_put_contents($_CLI['output-config'], $output);
diff --git a/documentation-po-update b/documentation-po-update
index 2c3e670..c1fecfa 100755
--- a/documentation-po-update
+++ b/documentation-po-update
@@ -24,7 +24,7 @@ git fetch -q origin
 newrev=`git log -n 1 --format=format:%H origin/master`
 
 # Is there some diff?
-if git diff --stat --exit-code $oldrev..$newrev -- doc/ ; then
+if git diff --stat --exit-code $oldrev..$newrev -- docs/ ; then
     exit 0
 fi
 
diff --git a/hooks/commits.php b/hooks/commits.php
index d50b931..c42c245 100644
--- a/hooks/commits.php
+++ b/hooks/commits.php
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * GitHub webhook to check Signed-Off-By in pull request commits.
  */
@@ -7,66 +8,66 @@
 
 define('PMAHOOKS', true);
 
-require_once('./lib/github.php');
+require_once __DIR__ . '/lib/github.php';
 
 github_verify_post();
 
 $contributing_url = 'https://github.com/phpmyadmin/phpmyadmin/blob/master/CONTRIBUTING.md';
 $guidelines_url = 'https://github.com/phpmyadmin/phpmyadmin/wiki/Developer_guidelines';
-$dco_link = 'https://github.com/phpmyadmin/phpmyadmin/blob/master/DCO';
+$dcoLink = 'https://github.com/phpmyadmin/phpmyadmin/blob/master/DCO';
 
 $message_sob = "<!-- PMABOT:SOB -->\n"
-    . "This commit is missing Signed-Off-By line to indicate "
+    . 'This commit is missing Signed-Off-By line to indicate '
     . "that you agree with phpMyAdmin Developer's Certificate of Origin. "
-    . "Please check [contributing documentation]("
+    . 'Please check [contributing documentation]('
     . $contributing_url
-    . ") for more information.";
+    . ') for more information.';
 
 $message_sob_invalid = "<!-- PMABOT:SOB --><!-- PMABOT:SOB:INVALID -->\n"
-    . "This commit has an invalid Signed-Off-By line, please use "
+    . 'This commit has an invalid Signed-Off-By line, please use '
     . "your full name for legal compliance reasons.\n"
-    . "Please check [Developer's Certificate of Origin]($dco_link)\n"
-    . "Here is a valid example: `Signed-off-by: Jane Smith <jane.smith@example.org>`";
+    . 'Please check [Developer\'s Certificate of Origin](' . $dcoLink . ")\n"
+    . 'Here is a valid example: `Signed-off-by: Jane Smith <jane.smith@example.org>`';
 
 $message_tab = "<!-- PMABOT:TAB -->\n"
-    . "This commit is using tab character for indentation instead "
-    . "of spaces, what is mandated by phpMyAdmin. Please check our "
-    . "[Developer guidelines]("
+    . 'This commit is using tab character for indentation instead '
+    . 'of spaces, what is mandated by phpMyAdmin. Please check our '
+    . '[Developer guidelines]('
     . $guidelines_url
-    . "#Indentation) for more information."
+    . '#Indentation) for more information.'
     . "\n\nOffending files: ";
 
 $message_space = "<!-- PMABOT:SPACE -->\n"
-    . "This commit contains trailing whitespace, "
-    . "what is prohibited in phpMyAdmin. Please check our "
-    . "[Developer guidelines]("
+    . 'This commit contains trailing whitespace, '
+    . 'what is prohibited in phpMyAdmin. Please check our '
+    . '[Developer guidelines]('
     . $guidelines_url
-    . "#Indentation) for more information."
+    . '#Indentation) for more information.'
     . "\n\nOffending files: ";
 
 $message_eol = "<!-- PMABOT:EOL -->\n"
-    . "This commit is using DOS end of line characters instead "
-    . "of UNIX ones, what is mandated by phpMyAdmin. Please check our "
-    . "[Developer guidelines]("
+    . 'This commit is using DOS end of line characters instead '
+    . 'of UNIX ones, what is mandated by phpMyAdmin. Please check our '
+    . '[Developer guidelines]('
     . $guidelines_url
-    . "#Indentation) for more information."
+    . '#Indentation) for more information.'
     . "\n\nOffending files: ";
 
 $message_commits = "<!-- PMABOT:COMMITS -->\n"
-    . "This pull requests contains too many commits, in most cases "
-    . "it is caused by wrong merge target. In case you have forked "
-    . "`master` branch you should also ask merging to `master` branch. "
-    . "See [GitHub documentation]"
-    . "(https://help.github.com/articles/creating-a-pull-request/) "
-    . "for more details.";
+    . 'This pull requests contains too many commits, in most cases '
+    . 'it is caused by wrong merge target. In case you have forked '
+    . '`master` branch you should also ask merging to `master` branch. '
+    . 'See [GitHub documentation]'
+    . '(https://help.github.com/articles/creating-a-pull-request/) '
+    . 'for more details.';
 
 /* Parse JSON */
 $data = json_decode($_POST['payload'], true);
 
 /* Check request data */
 if (isset($data['zen']) && isset($data['hook']) && isset($data['hook_id'])) {
-    json_response(array('pong' => true));
-    die();
+    json_response(['pong' => true]);
+    die;
 }
 
 if (! isset($data['pull_request']) || ! isset($data['action'])) {
@@ -75,13 +76,13 @@
 
 /* We don't care about closed requests */
 if ($data['action'] == 'closed') {
-    die();
+    die;
 }
 
 /* Check number of commits */
 if ($data['pull_request']['commits'] > 50) {
     github_comment_pull($data['repository']['full_name'], $data['pull_request']['number'], $message_commits);
-    die();
+    die;
 }
 
 /* Parse repository name */
@@ -90,7 +91,7 @@
 /* List commits in the pull request */
 $commits = github_pull_commits($data['repository']['full_name'], $data['pull_request']['number']);
 
-$comments = array();
+$comments = [];
 
 /* Process commits in the pull request */
 foreach ($commits as $commit) {
@@ -107,71 +108,81 @@
     }
 
     /* Check for missing SOB */
-    if ( ! preg_match("@\nSigned-off-by:@i", $commit['commit']['message'])) {
+    if (! preg_match("@\nSigned-off-by:@i", $commit['commit']['message'])) {
         if (strpos($comments_text, '<!-- PMABOT:SOB -->') === false) {
             github_comment_commit($repo_name, $commit['sha'], $message_sob);
-            $comments[] = array(
+            $comments[] = [
                 'type' => 'SOB',
                 'commit' => $commit['sha'],
                 'message' => $commit['commit']['message'],
-            );
+            ];
         }
     } else {
         /* Check for invalid SOB */
-        if ( ! preg_match("@\nSigned-off-by: +(?:[\p{L}\-\.]+\s){2,}<.*>@iu", $commit['commit']['message'])) {
+        if (! preg_match("@\nSigned-off-by: +(?:[\p{L}\-\.]+\s){2,}<.*>@iu", $commit['commit']['message'])) {
             if (strpos($comments_text, '<!-- PMABOT:SOB:INVALID -->') === false) {
                 github_comment_commit($repo_name, $commit['sha'], $message_sob_invalid);
-                $comments[] = array(
+                $comments[] = [
                     'type' => 'SOB',
                     'commit' => $commit['sha'],
                     'message' => $commit['commit']['message'],
-                );
+                ];
             }
         }
     }
 
     /* Check for tab or trailing whitespace in diff */
     $detail = github_commit_detail($data['repository']['full_name'], $commit['sha']);
-    $files_tab = array();
-    $files_space = array();
-    $files_eol = array();
+    $files_tab = [];
+    $files_space = [];
+    $files_eol = [];
     foreach ($detail['files'] as $file) {
         if (preg_match("@\n\+[^\n]*\t@", $file['patch'])) {
-            if (! preg_match('/^libraries\/advisory_rules.*\.txt$/', $file['filename'])) {
+            $matchAdvisory = preg_match('/^libraries\/advisory_rules.*\.txt$/', $file['filename']);
+            // Example: https://api.github.com/repos/phpmyadmin/phpmyadmin/commits/b7cb1c84bacc5de1490a4e5cf241401cbfa5c105
+            $isABaseLineFile = in_array($file['filename'], [
+                'phpstan-baseline.neon',
+                'psalm-baseline.xml',
+            ]);
+            if (! $matchAdvisory && ! $isABaseLineFile) {
                 $files_tab[] = $file['filename'];
             }
         }
         if (preg_match("@\n\+[^\n]* \n@", $file['patch'])) {
             $files_space[] = $file['filename'];
         }
-        if (preg_match("@\n\+[^\n]*\r@", $file['patch'])) {
-            $files_eol[] = $file['filename'];
+        if (! preg_match("@\n\+[^\n]*\r@", $file['patch'])) {
+            continue;
         }
+
+        $files_eol[] = $file['filename'];
     }
     if (count($files_tab) && strpos($comments_text, 'PMABOT:TAB') === false) {
         github_comment_commit($repo_name, $commit['sha'], $message_tab . implode(', ', $files_tab));
-        $comments[] = array(
+        $comments[] = [
             'type' => 'TAB',
             'commit' => $commit['sha'],
             'message' => $commit['commit']['message'],
-        );
+        ];
     }
     if (count($files_space) && strpos($comments_text, 'PMABOT:SPACE') === false) {
         github_comment_commit($repo_name, $commit['sha'], $message_space . implode(', ', $files_space));
-        $comments[] = array(
+        $comments[] = [
             'type' => 'SPACE',
             'commit' => $commit['sha'],
             'message' => $commit['commit']['message'],
-        );
+        ];
     }
-    if (count($files_eol) && strpos($comments_text, 'PMABOT:EOL') === false) {
-        github_comment_commit($repo_name, $commit['sha'], $message_eol . implode(', ', $files_eol));
-        $comments[] = array(
-            'type' => 'EOL',
-            'commit' => $commit['sha'],
-            'message' => $commit['commit']['message'],
-        );
+    if (! count($files_eol) || strpos($comments_text, 'PMABOT:EOL') !== false) {
+        continue;
     }
+
+    github_comment_commit($repo_name, $commit['sha'], $message_eol . implode(', ', $files_eol));
+    $comments[] = [
+        'type' => 'EOL',
+        'commit' => $commit['sha'],
+        'message' => $commit['commit']['message'],
+    ];
 }
 
-json_response(array('comments' => $comments));
+json_response(['comments' => $comments]);
diff --git a/hooks/composer.json b/hooks/composer.json
index 2160b81..b7f7f88 100644
--- a/hooks/composer.json
+++ b/hooks/composer.json
@@ -2,7 +2,7 @@
     "name": "phpmyadmin/scripts",
     "type": "project",
     "require": {
-        "phpmailer/phpmailer": "^6.0"
+        "phpmailer/phpmailer": "^6.7.1"
     },
     "homepage": "https://github.com/phpmyadmin/scripts",
     "readme": "https://github.com/phpmyadmin/scripts#readme",
diff --git a/hooks/config-example.php b/hooks/config-example.php
index 5bfd5be..c3ff9aa 100644
--- a/hooks/config-example.php
+++ b/hooks/config-example.php
@@ -1,8 +1,8 @@
 <?php
 
-define('GITHUB_HOOK_SECRET', 'passwordddddd');
+define('GITHUB_HOOK_SECRET', 'xxxxxxxxxxxxxxREPLACEMExxxxxxxxxxxxxxxxxx');
 define('GITHUB_USERNAME', 'phpmyadmin-bot');
-define('GITHUB_PASSWORD', 'password');
+define('GITHUB_TOKEN', 'xxxxxxxxxxxxxxREPLACEMExxxxxxxxxxxxxxxxxx');// https://github.com/settings/tokens/new scopes: repo, workflow
 define('SMTP_SEND_TO', 'maillist@example.org');
 define('SMTP_SEND_FROM_EMAIL', 'mail@example.org');// Optional
 define('SMTP_SEND_BACK_TO', 'maillist@example.org');// Optional
@@ -12,3 +12,6 @@
 define('SMTP_MODE', 'tls');// ssl or tls
 define('SMTP_USERNAME', 'mail@example.org');
 define('SMTP_PASSWORD', 'P@ssw0rd12345');
+
+define('WEBSITE_HOOK', 'https://example.org/replaceme-by-an-url');
+define('DOCS_HOOK', 'https://example.org/replaceme-by-an-url');
diff --git a/hooks/create.php b/hooks/create.php
index df47885..13a850f 100644
--- a/hooks/create.php
+++ b/hooks/create.php
@@ -7,7 +7,7 @@
 
 define('PMAHOOKS', true);
 
-require_once('./lib/github.php');
+require_once __DIR__ . '/lib/github.php';
 
 github_verify_post();
 
diff --git a/hooks/create_release.php b/hooks/create_release.php
index f0ad9c5..9047205 100644
--- a/hooks/create_release.php
+++ b/hooks/create_release.php
@@ -7,7 +7,7 @@
 
 define('PMAHOOKS', true);
 
-require_once('./lib/github.php');
+require_once __DIR__ . '/lib/github.php';
 
 github_verify_post();
 
diff --git a/hooks/docs.php b/hooks/docs.php
index 10fef39..487e035 100644
--- a/hooks/docs.php
+++ b/hooks/docs.php
@@ -7,6 +7,6 @@
 
 define('PMAHOOKS', true);
 
-require_once('./lib/github.php');
+require_once __DIR__ . '/lib/github.php';
 
 trigger_docs_render();
diff --git a/hooks/lib/github.php b/hooks/lib/github.php
index 1f63dad..56f19bc 100644
--- a/hooks/lib/github.php
+++ b/hooks/lib/github.php
@@ -3,25 +3,29 @@
  * GitHub operations layer.
  */
 
-if (!defined('PMAHOOKS')) {
+if (! defined('PMAHOOKS')) {
     fail('Invalid invocation!');
 }
 
-require_once('./config.php');
+require_once __DIR__ . '/../config.php';
 
-$curl_base_opts = array(
-    CURLOPT_USERPWD => GITHUB_USERNAME . ':' . GITHUB_PASSWORD,
+$curlBaseOpts = [
     CURLOPT_USERAGENT => 'phpMyAdmin-bot',
     CURLOPT_RETURNTRANSFER => 1,
-    CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
-    CURLOPT_HTTPHEADER => array('Content-Type: application/json'),
-);
+    CURLOPT_HTTPHEADER => [
+        'Content-Type: application/json',
+        'Authorization: token ' . GITHUB_TOKEN,
+
+    ],
+];
 
 /**
  * Builds the body of the email to send
  *
- * @param stdClass $inputData The JSON payload
  * @see https://developer.github.com/v3/activity/events/types/#pushevent
+ *
+ * @param stdClass $inputData The JSON payload
+ *
  * @return stdClass {
  *       "repoName": "org/repo",
  *       "compare": "URL TO COMPARE PAGE",
@@ -32,47 +36,48 @@
  *       "authorEmail": "williamdes@wdes.fr"
  * }
  */
-function gihub_webhook_push(stdClass $inputData): stdClass
+function github_webhook_push(stdClass $inputData): stdClass
 {
     $msg  = '';
     $msg .= 'Branch: ' . $inputData->ref . PHP_EOL;
     $msg .= 'Home: ' . $inputData->repository->html_url . PHP_EOL;
 
     foreach ($inputData->commits as $commit) {
-    $msg .= 'Commit: ' . $commit->id . PHP_EOL;
-    $msg .= $commit->url . PHP_EOL;
-    $msg .= 'Author: ' . $commit->author->name . ' <' . $commit->author->email . '>' . PHP_EOL;
-
-    $date = new DateTime($commit->timestamp);
-    $msg .= 'Date: ' . $date->format('Y-m-d (D, m F Y) P') . PHP_EOL . PHP_EOL;
-
-    $msg .= 'Changed paths: ' . PHP_EOL;
-    foreach ($commit->added as $file) {
-        $msg .= 'A ' . $file . PHP_EOL;
-    }
-    foreach ($commit->modified as $file) {
-        $msg .= 'M ' . $file . PHP_EOL;
-    }
-    foreach ($commit->removed as $file) {
-        $msg .= 'D ' . $file . PHP_EOL;
-    }
-    $msg .= PHP_EOL . 'Log Message:' . PHP_EOL . '-----------' . PHP_EOL;
-    $msg .= $commit->message . PHP_EOL . PHP_EOL;
-
+        $msg .= 'Commit: ' . $commit->id . PHP_EOL;
+        $msg .= $commit->url . PHP_EOL;
+        $msg .= 'Author: ' . $commit->author->name . ' <' . $commit->author->email . '>' . PHP_EOL;
+
+        $date = new DateTime($commit->timestamp);
+        $msg .= 'Date: ' . $date->format('Y-m-d (D, m F Y) P') . PHP_EOL . PHP_EOL;
+
+        $msg .= 'Changed paths: ' . PHP_EOL;
+        foreach ($commit->added as $file) {
+            $msg .= 'A ' . $file . PHP_EOL;
+        }
+        foreach ($commit->modified as $file) {
+            $msg .= 'M ' . $file . PHP_EOL;
+        }
+        foreach ($commit->removed as $file) {
+            $msg .= 'D ' . $file . PHP_EOL;
+        }
+        $msg .= PHP_EOL . 'Log Message:' . PHP_EOL . '-----------' . PHP_EOL;
+        $msg .= $commit->message . PHP_EOL . PHP_EOL;
     }
 
     $data = new stdClass();
     $data->repoName            = $inputData->repository->full_name;
     $data->compare             = $inputData->compare;
     $data->emailBody           = $msg;
-    $data->headCommitTitle     = explode("\n", $inputData->commits[0]->message)[0];
-    $data->headCommitShortHash = substr($inputData->commits[0]->id, 0, 6);
-    $data->authorName          = $inputData->commits[0]->author->name;
-    $data->authorEmail         = $inputData->commits[0]->author->email;
+    // Commits can be empty and head_commit filled in case of a tag
+    $firstCommit = $inputData->commits[0] ?? $inputData->head_commit;
+    $data->headCommitTitle     = explode("\n", $firstCommit->message)[0];
+    $data->headCommitShortHash = substr($firstCommit->id, 0, 6);
+    $data->authorName          = $firstCommit->author->name;
+    $data->authorEmail         = $firstCommit->author->email;
+
     return $data;
 }
 
-
 /**
  * Verifies signature from GitHub
  */
@@ -81,24 +86,26 @@ function github_verify_post()
     if (empty(GITHUB_HOOK_SECRET)) {
         fail('Missing hook secret configuration!');
     }
-    if (!isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
+    if (! isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
         fail("HTTP header 'X-Hub-Signature' is missing.");
-    } elseif (!extension_loaded('hash')) {
+    } elseif (! extension_loaded('hash')) {
         fail("Missing 'hash' extension to check the secret code validity.");
     }
 
-    list($algo, $hash) = explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'], 2) + array('', '');
-    if (!in_array($algo, array('sha1', 'sha256', 'sha512'), true)) {
+    [$algo, $hash] = explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'], 2) + ['', ''];
+    if (! in_array($algo, ['sha1', 'sha256', 'sha512'], true)) {
         fail("Hash algorithm '$algo' is not allowed.");
     }
-    if (!in_array($algo, hash_algos(), true)) {
+    if (! in_array($algo, hash_algos(), true)) {
         fail("Hash algorithm '$algo' is not supported.");
     }
     $rawPost = file_get_contents('php://input');
     $newhash = hash_hmac($algo, $rawPost, GITHUB_HOOK_SECRET);
-    if (! hash_equals($hash, $newhash)) {
-        fail('Hook secret does not match.');
+    if (hash_equals($hash, $newhash)) {
+        return;
     }
+
+    fail('Hook secret does not match.');
 }
 
 /**
@@ -106,26 +113,30 @@ function github_verify_post()
  */
 function github_make_release($repo, $tag, $version, $description)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
 
-    $result = array(
-        'release' => array(
+    $result = [
+        'release' => [
             'project' => $repo,
             'tag' => $tag,
             'version' => $version,
-            'description' => $description
-        )
-    );
+            'description' => $description,
+        ],
+    ];
 
     //set the url, number of POST vars, POST data
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $repo . '/releases');
     curl_setopt($ch, CURLOPT_POST, 1);
-    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array('tag_name' => $tag, 'name' => $version, 'body' => $description)));
+    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['tag_name' => $tag, 'name' => $version, 'body' => $description]));
 
     //execute post
     $response = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -139,17 +150,21 @@ function github_make_release($repo, $tag, $version, $description)
  */
 function github_comment_pull($repo, $pullid, $comment)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
 
     //set the url, number of POST vars, POST data
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $repo . '/issues/' . $pullid . '/comments');
     curl_setopt($ch, CURLOPT_POST, 1);
-    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array('body' => $comment)));
+    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['body' => $comment]));
 
     //execute post
     $result = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -161,17 +176,21 @@ function github_comment_pull($repo, $pullid, $comment)
  */
 function github_comment_commit($repo, $sha, $comment)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
 
     //set the url, number of POST vars, POST data
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $repo . '/commits/' . $sha . '/comments');
     curl_setopt($ch, CURLOPT_POST, 1);
-    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array('body' => $comment)));
+    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['body' => $comment]));
 
     //execute post
     $result = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -183,13 +202,17 @@ function github_comment_commit($repo, $sha, $comment)
  */
 function github_pull_commits($repo, $pullid)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $repo . '/pulls/' . $pullid . '/commits');
 
     //execute post
     $result = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -197,17 +220,21 @@ function github_pull_commits($repo, $pullid)
 }
 
 /**
- * Returns diff of pull request commit detal.
+ * Returns diff of pull request commit detail.
  */
 function github_commit_detail($repo, $commit)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $repo . '/commits/' . $commit);
 
     //execute post
     $result = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -215,17 +242,21 @@ function github_commit_detail($repo, $commit)
 }
 
 /**
- * Returns diff of pull request commit detal.
+ * Returns diff of pull request commit detail.
  */
 function github_commit_comments($repo, $sha)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $repo . '/commits/' . $sha . '/comments');
 
     //execute post
     $result = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -237,13 +268,17 @@ function github_commit_comments($repo, $sha)
  */
 function github_pull_diff($repo, $pullid)
 {
+    global $curlBaseOpts;
+
     $ch = curl_init();
-    curl_setopt_array($ch, $GLOBALS['curl_base_opts']);
+    curl_setopt_array($ch, $curlBaseOpts);
     curl_setopt($ch, CURLOPT_URL, 'https://github.com/' . $repo . '/pull/' . $pullid . '.patch');
 
     //execute post
     $result = curl_exec($ch);
 
+    continueOrFailOnHttpCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+
     //close connection
     curl_close($ch);
 
@@ -273,14 +308,12 @@ function trigger_docs_render()
  *
  * @param mixed $data JSON data to print
  */
-function json_response($data, $status='success', $message=null)
+function json_response($data, $status = 'success', $message = null)
 {
     header('Content-Type: application/json; charset=UTF-8');
     header('X-Content-Type-Options: nosniff');
 
-    $response = array(
-        'status' => $status
-    );
+    $response = ['status' => $status];
     if (isset($data)) {
         $response['data'] = $data;
     }
@@ -291,13 +324,20 @@ function json_response($data, $status='success', $message=null)
     echo json_encode($response, JSON_PRETTY_PRINT);
 }
 
+function continueOrFailOnHttpCode(int $httpCode): void
+{
+    if ($httpCode > 204) {
+        fail('Request failed with a code ' . $httpCode);
+    }
+}
+
 /**
  * Terminates request with error
  */
-function fail($message, $code=500)
+function fail($message, $code = 500)
 {
     http_response_code($code);
     json_response(null, 'error', $message);
 
-    die();
+    die;
 }
diff --git a/hooks/push.php b/hooks/push.php
index 6c51bb2..e1d404c 100644
--- a/hooks/push.php
+++ b/hooks/push.php
@@ -11,15 +11,15 @@
 
 define('PMAHOOKS', true);
 
-require_once(__DIR__.'/lib/github.php');
+require_once __DIR__ . '/lib/github.php';
 
-if ( !is_file(__DIR__.'/vendor/autoload.php')) {
-    fail("Run composer install");
+if ( !is_file(__DIR__ . '/vendor/autoload.php')) {
+    fail('Run composer install');
     exit;
 }
 
 //Load Composer's autoloader
-require_once(__DIR__.'/vendor/autoload.php');
+require_once __DIR__ . '/vendor/autoload.php';
 
 github_verify_post();
 
@@ -36,7 +36,7 @@
     die();
 }
 
-$data = gihub_webhook_push($inputData);
+$data = github_webhook_push($inputData);
 
 
 $mail = new PHPMailer(true);
diff --git a/hooks/website.php b/hooks/website.php
index 593ae55..f8a130f 100644
--- a/hooks/website.php
+++ b/hooks/website.php
@@ -7,7 +7,7 @@
 
 define('PMAHOOKS', true);
 
-require_once('./lib/github.php');
+require_once __DIR__ . '/lib/github.php';
 
 github_verify_post();
 
diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore
new file mode 100644
index 0000000..a76642c
--- /dev/null
+++ b/infrastructure/.gitignore
@@ -0,0 +1,2 @@
+.env*
+!.env.dist
diff --git a/infrastructure/sentry/.env.dist b/infrastructure/sentry/.env.dist
new file mode 100644
index 0000000..37c4016
--- /dev/null
+++ b/infrastructure/sentry/.env.dist
@@ -0,0 +1,7 @@
+SENTRY_SECRET_KEY=
+POSTGRES_PASSWORD=
+
+POSTGRES_DATA_VOLUME=
+SENTRY_DATA_VOLUME=
+
+SENTRY_SERVER_EMAIL=
diff --git a/infrastructure/sentry/README.md b/infrastructure/sentry/README.md
new file mode 100644
index 0000000..d456847
--- /dev/null
+++ b/infrastructure/sentry/README.md
@@ -0,0 +1,27 @@
+# phpMyAdmin Sentry instance
+
+## Required
+
+- `docker`
+- `docker-compose`
+
+## Initialization
+
+Copy `.env.dist` to `.env.`, fill the values.
+
+Run `./dockerl exec sentry sentry upgrade` to apply the migrations.
+
+The CLI will ask if you want to create an user, say "Y" and fill the email and password.
+
+If you missed your chance, use this command to have another one: `./dockerl exec sentry sentry createuser`
+
+Run `./dockerl exec sentry pip install sentry-slack` to install the Sentry Slack integration.
+Run `./dockerl exec sentry pip install sentry-github` to install the Sentry GitHub integration.
+
+Login into http://localhost:9000/ or something similar.
+
+## Start it
+
+Run `./dockerl up` or `./dockerl up -d` for detached mode.
+
+This command is a shortcut to docker compose with the right project name.
diff --git a/infrastructure/sentry/docker-compose.yml b/infrastructure/sentry/docker-compose.yml
new file mode 100644
index 0000000..cfa2e0b
--- /dev/null
+++ b/infrastructure/sentry/docker-compose.yml
@@ -0,0 +1,93 @@
+version: '2.3'
+
+services:
+  redis:
+    restart: on-failure:5
+    image: redis:6.0-alpine
+    healthcheck:
+        test: ["CMD", "redis-cli", "ping"]
+        interval: 1s
+        timeout: 3s
+        retries: 30
+
+  postgres:
+    restart: on-failure:5
+    image: postgres:13-alpine
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U sentry"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 10s
+    environment:
+      POSTGRES_USER: sentry
+      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
+      POSTGRES_DB: sentry
+    volumes:
+     - ${POSTGRES_DATA_VOLUME}:/var/lib/postgresql/data
+  smtp:
+    restart: unless-stopped
+    # Use this value to have correct SMTP HELO value
+    hostname: hertz.phpmyadmin.net
+    image: tianon/exim4
+
+  sentry:
+    image: sentry:9.1.2
+    depends_on:
+      postgres:
+        condition: service_healthy
+      redis:
+        condition: service_healthy
+    ports:
+     - 9000:9000
+    environment:
+      SENTRY_SECRET_KEY: "${SENTRY_SECRET_KEY}"
+      SENTRY_POSTGRES_HOST: postgres
+      SENTRY_DB_USER: sentry
+      SENTRY_DB_PASSWORD: "${POSTGRES_PASSWORD}"
+      SENTRY_REDIS_HOST: redis
+      SENTRY_FILESTORE_DIR: /data/filestorage
+      SENTRY_SERVER_EMAIL: "${SENTRY_SERVER_EMAIL}"
+      SENTRY_EMAIL_HOST: smtp
+    volumes:
+     - ${SENTRY_DATA_VOLUME}:/data
+
+  cron:
+    image: sentry:9.1.2
+    depends_on:
+      postgres:
+        condition: service_healthy
+      redis:
+        condition: service_healthy
+    command: "sentry run cron"
+    environment:
+      SENTRY_SECRET_KEY: "${SENTRY_SECRET_KEY}"
+      SENTRY_POSTGRES_HOST: postgres
+      SENTRY_DB_USER: sentry
+      SENTRY_DB_PASSWORD: "${POSTGRES_PASSWORD}"
+      SENTRY_REDIS_HOST: redis
+      SENTRY_FILESTORE_DIR: /data/filestorage
+      SENTRY_SERVER_EMAIL: "${SENTRY_SERVER_EMAIL}"
+      SENTRY_EMAIL_HOST: smtp
+    volumes:
+     - ${SENTRY_DATA_VOLUME}:/data
+
+  worker:
+    image: sentry:9.1.2
+    depends_on:
+      postgres:
+        condition: service_healthy
+      redis:
+        condition: service_healthy
+    command: "sentry run worker"
+    environment:
+      SENTRY_SECRET_KEY: "${SENTRY_SECRET_KEY}"
+      SENTRY_POSTGRES_HOST: postgres
+      SENTRY_DB_USER: sentry
+      SENTRY_DB_PASSWORD: "${POSTGRES_PASSWORD}"
+      SENTRY_REDIS_HOST: redis
+      SENTRY_FILESTORE_DIR: /data/filestorage
+      SENTRY_SERVER_EMAIL: "${SENTRY_SERVER_EMAIL}"
+      SENTRY_EMAIL_HOST: smtp
+    volumes:
+     - ${SENTRY_DATA_VOLUME}:/data
diff --git a/infrastructure/sentry/dockerl b/infrastructure/sentry/dockerl
new file mode 100755
index 0000000..5d5b951
--- /dev/null
+++ b/infrastructure/sentry/dockerl
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+docker-compose -p phpmyadmin-sentry $@
diff --git a/monitoring/README.rst b/monitoring/README.rst
new file mode 100644
index 0000000..619d9d0
--- /dev/null
+++ b/monitoring/README.rst
@@ -0,0 +1,13 @@
+Scripts for monitoring downloads
+================================
+
+These scripts are for monitoring daily snapshot downloads as well as the Composer and Docker packages.
+
+Of particular note, the daily snapshot tool requires some extra Python package, BeautifulSoup4.
+
+On Debian, this is probably available as ``python3-bs4``.
+
+Aside from Debian, I suggest creating a venv for this. I did it with ``python3 -m venv daily_snapshot_venv`` then source that with
+``source daily_snapshot_venv/bin/activate``.
+Install 'beautifulsoup4' to the venv.
+
diff --git a/monitoring/check_phpmyadmin_composer.sh b/monitoring/check_phpmyadmin_composer.sh
new file mode 100755
index 0000000..a2d2b55
--- /dev/null
+++ b/monitoring/check_phpmyadmin_composer.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+supported_versions=$(curl -fsSL 'https://www.phpmyadmin.net/home_page/version.json'|jq -r '.releases|.[]|.version')
+working_dir="$HOME/tmp/composer-test"
+
+mkdir -p "$working_dir"
+cd "$working_dir" || exit 1
+
+test_version() {
+	ver=$1
+	echo "Processing $ver"
+
+  composer create-project -q phpmyadmin/phpmyadmin "$ver" "$ver" > /dev/null
+  grep "$ver" "$ver/ChangeLog" > /dev/null || echo "Failed to find version $ver in $ver/ChangeLog" 
+  rm -rf "$ver"
+}
+
+while read -r full_version
+# https://unix.stackexchange.com/questions/9784/how-can-i-read-line-by-line-from-a-variable-in-bash
+do
+  test_version "$full_version"
+done < <(printf '%s\n' "$supported_versions")
\ No newline at end of file
diff --git a/monitoring/check_phpmyadmin_daily_snapshots.py b/monitoring/check_phpmyadmin_daily_snapshots.py
new file mode 100755
index 0000000..19d8388
--- /dev/null
+++ b/monitoring/check_phpmyadmin_daily_snapshots.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+# I suggest creating a venv for this. I did it with `python3 -m venv daily_snapshot_venv` then source that with `source daily_snapshot_venv/bin/activate`.
+# Install 'beautifulsoup4' to the venv
+
+# This program checks that the phpMyAdmin daily downloads are within a day or two of the current time.
+# We allow an extra day of wiggle room because of the possibility of working in different time zones.
+#
+# Written by Isaac Bennetch <bennetch@gmail.com>
+
+import urllib.request
+try:
+    from bs4 import BeautifulSoup
+except ModuleNotFoundError:
+    print("The BeautifulSoup module is not available. Please make sure you have the proper module or are running the correct venv.")
+    raise SystemExit
+import datetime
+
+#############
+# Main code #
+#############
+
+
+download_url = 'https://www.phpmyadmin.net/downloads/'
+
+page = urllib.request.urlopen(download_url)
+soup = BeautifulSoup(page, 'html.parser')
+full_text = soup.get_text()
+split = full_text.split("generated")
+
+today_date = datetime.date.today()
+
+# Process each instance of the keyword on the page, because we can have multiple QA and master versions
+for substring in split:
+  # Take only the date, not the entire page following it
+  generated_date = substring.split()[0]
+  # Strip off the trailing comma
+  if generated_date.endswith(','):
+    generated_date = generated_date[:-1]
+  if generated_date != 'phpMyAdmin':
+    converted_date = datetime.datetime.strptime(generated_date, '%Y-%m-%d').date()
+    difference = today_date - converted_date
+    # We give an extra day as a buffer to account for time zones
+    if difference > datetime.timedelta(days=1):
+      print("Problem found")
+      print("Current date: " + today_date.strftime('%Y-%m-%d'))
+      print("Snapshot date: " + generated_date)
+    else:
+      print("Everything is groovy")
+
diff --git a/monitoring/check_phpmyadmin_docker.sh b/monitoring/check_phpmyadmin_docker.sh
new file mode 100755
index 0000000..8920bc3
--- /dev/null
+++ b/monitoring/check_phpmyadmin_docker.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+# This script will get a list of currently supported versions from phpmyadmin.net.
+# For each version, it pulls the Docker image for each flavor, including major and minor
+# version shortcuts (5, 5.0, and 5.0.0). We check that the expected version is listed in ChangeLog.
+#
+# Future versions could use jq to parse package.json for the "version" field
+#
+# Written by Isaac Bennetch <bennetch@gmail.com>
+
+supported_versions=$(curl -fsSL 'https://www.phpmyadmin.net/home_page/version.json'|jq -r '.releases|.[]|.version')
+
+if [ "$1" == "-h" ] || [ "$1" == "--help" ]
+then
+  echo "Checks whether the docker builds are current to the versions presented in the version.json file from phpmyadmin.net"
+  echo ""
+  echo "Possible arguments:"
+  echo "  -v --verbose    Prints some progress output (usually runs silently)"
+  echo "  -h --help       Prints this help message"
+  exit ""
+fi
+
+if [ "$1" == "-v" ] || [ "$1" == "--verbose" ]
+then
+  verbose=true
+else
+  verbose=false
+fi
+
+test_version() {
+	ver=$1
+  nodots=${ver//[-.]/}
+  container=ptest-$nodots
+  for flavor in "$ver" "$ver-fpm" "$ver-fpm-alpine"
+  do
+    if [ "$verbose" == true ]
+    then
+      echo "Processing $flavor"
+    fi
+
+    docker pull -q phpmyadmin/phpmyadmin:"$flavor" > /dev/null
+    docker run --name "$container" -d phpmyadmin/phpmyadmin:"$flavor" > /dev/null
+    docker exec -t "$container" grep "$ver" ChangeLog > /dev/null || echo "Failed to find version $ver in $container"
+    docker container stop "$container" > /dev/null
+    docker container rm "$container" > /dev/null
+  done
+}
+
+while read -r full_version
+# https://unix.stackexchange.com/questions/9784/how-can-i-read-line-by-line-from-a-variable-in-bash
+do
+	major="${full_version:0:1}"
+	minor="${full_version:0:3}"
+	test_version "$full_version"
+	test_version "$minor"
+	test_version "$major"
+done < <(printf '%s\n' "$supported_versions")
diff --git a/phpmyadmin-report.php b/phpmyadmin-report.php
new file mode 100755
index 0000000..b15b2de
--- /dev/null
+++ b/phpmyadmin-report.php
@@ -0,0 +1,659 @@
+#!/bin/php
+<?php
+
+declare(strict_types=1);
+
+if (PHP_SAPI !== 'cli') {
+    exit;
+}
+
+if (PHP_VERSION_ID < 80100) {// 80 1 00
+    echo 'You need to use PHP 8.1.0 or above to run this script.' . PHP_EOL;
+    echo 'Current detected version: (' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . ') (' . PHP_VERSION_ID . ').' . PHP_EOL;
+    exit(1);
+}
+
+class Reports
+{
+    private const PROJECTS = [
+        'github.com' => [
+            'phpmyadmin/phpmyadmin',
+            'phpmyadmin/docker',
+            'phpmyadmin/website',
+            'phpmyadmin/sql-parser',
+            'phpmyadmin/motranslator',
+            'phpmyadmin/shapefile',
+            'phpmyadmin/simple-math',
+            'phpmyadmin/localized_docs',
+            'phpmyadmin/error-reporting-server',
+            'phpmyadmin/twig-i18n-extension',
+            'docker-library/docs',
+            'docker-library/official-images',
+            // Disable private repos
+            // 'phpmyadmin/phpmyadmin-security',
+            // 'phpmyadmin/private',
+        ],
+        // Token scopes: read_api
+        // See: https://salsa.debian.org/phpmyadmin-team/phpmyadmin/-/settings/access_tokens
+        'gitlab.com' => [],
+        'salsa.debian.org' => [
+            'phpmyadmin-team/phpmyadmin',
+            'phpmyadmin-team/twig-i18n-extension',
+            'phpmyadmin-team/mariadb-mysql-kbs',
+            'phpmyadmin-team/motranslator',
+            'phpmyadmin-team/sql-parser',
+            'phpmyadmin-team/shapefile',
+            'phpmyadmin-team/tcpdf',
+        ],
+    ];
+
+    private const IGNORE_WORDS = [
+        'Translated using Weblate',
+        'Merge branch ',
+        'Merge remote-tracking branch ',
+        'Update d/changelog',// Debian commits for debian/changelog
+    ];
+
+    /**
+     * @var array{
+     * type: "GitHub"|"GitLab", slug: string, data: array[] }
+     * }[]
+     */
+    private array $commitsStorage = [];
+
+    /**
+     * @var array{
+     * type: "GitHub"|"GitLab", slug: string, data: array[] }
+     * }[]
+     */
+    private array $issuesStorage = [];
+
+    private bool $quietMode = false;
+    private ?DateTimeImmutable $startDate = null;
+    private ?DateTimeImmutable $endDate = null;
+    private ?string $outputJsonData = null;
+    private mixed $outputRender = STDOUT;
+    private string $monthMode = 'none';
+    private string $outputMode = 'none';
+    // Tilde expansion: https://unix.stackexchange.com/a/151852/155610
+    private string $configFile = '~/.config/phpmyadmin';
+
+    public function run(): void
+    {
+        $shortopts  = '';
+        $shortopts .= 'qh'; // These options do not accept values
+
+        $longopts  = [
+            'output:',      // Required value
+            'output-json:', // Required value
+            'config:',      // Required value
+            'month:',       // Required value
+            'optional::',   // Optional value
+            'start-date::', // Optional value
+            'end-date::',   // Optional value
+            'help',         // No value
+            'quiet',        // No value
+            'last-month',   // No value
+            'current-month',// No value
+            'next-month',   // No value
+            'by-week',      // No value
+        ];
+        $options = getopt($shortopts, $longopts);
+
+        $this->checkExtensions();
+
+        if ($options === []) {
+            $this->printHelp();
+        }
+
+        foreach ($options as $optionName => $optionValue) {
+            if ($optionName === 'help' || $optionName === 'h') {
+                $this->printHelp();
+            }
+
+            if ($optionName === 'last-month') {
+                $this->monthMode = 'last';
+            }
+
+            if ($optionName === 'current-month') {
+                $this->monthMode = 'current';
+            }
+
+            if ($optionName === 'next-month') {
+                $this->monthMode = 'next';
+            }
+
+            if ($optionName === 'next-month') {
+                $this->monthMode = 'next';
+            }
+
+            if ($optionName === 'month') {
+                $this->monthMode = 'custom';
+                $this->startDate = new DateTimeImmutable('@' . strtotime('first day of ' . $optionValue . ' UTC'));
+                $this->endDate = new DateTimeImmutable('@' . strtotime('last day of ' . $optionValue . ' UTC'));
+            }
+
+            if ($optionName === 'start-date') {
+                $this->monthMode = 'custom';
+                if ($optionValue === false) {
+                    $this->quitError('The option --start-date needs a value, use the equal sign.');
+                }
+                $this->startDate = new DateTimeImmutable($optionValue . 'T00:00:00Z');
+            }
+
+            if ($optionName === 'end-date') {
+                $this->monthMode = 'custom';
+                if ($optionValue === false) {
+                    $this->quitError('The option --end-date needs a value, use the equal sign.');
+                }
+                $this->endDate = new DateTimeImmutable($optionValue . 'T23:59:59Z');
+            }
+
+            if ($optionName === 'quiet' || $optionName === 'q') {
+                $this->quietMode = true;
+            }
+
+            if ($optionName === 'config') {
+                $this->configFile = $optionValue;
+            }
+
+            if ($optionName === 'output') {
+                $this->outputRender = $optionValue;
+            }
+
+            if ($optionName === 'by-week') {
+                $this->outputMode = 'by-week';
+            }
+
+            if ($optionName !== 'output-json') {
+                continue;
+            }
+
+            $this->outputJsonData = $optionValue;
+        }
+
+        if ($this->monthMode === 'none') {
+            $this->quitError('You need to specify a month mode, using cli: --{current,last,next}-month');
+        }
+
+        $this->configFile = $this->expandTilde($this->configFile);
+        $this->detectCheckConfig();
+        $this->loadDates();
+
+        $this->readConfig();
+        $this->printFinalData();
+        $this->renderFinalData();
+    }
+
+    /**
+     * @source https://compwright.com/2013-09-03/tilde-expansion-in-php/
+     */
+    private function expandTilde(string $path): string
+    {
+        if (function_exists('posix_getuid') && strpos($path, '~') !== false) {
+            $info = posix_getpwuid(posix_getuid());
+
+            return str_replace('~', $info['dir'], $path);
+        }
+
+        return $path;
+    }
+
+    private function printHelp(): void
+    {
+        fwrite(STDOUT, 'Usage:' . "\n");
+        fwrite(STDOUT, '  Help: ./phpmyadmin-report.php -h' . "\n");
+        fwrite(STDOUT, '  Help: ./phpmyadmin-report.php --help' . "\n");
+        fwrite(STDOUT, '  Turn off debug: ./phpmyadmin-report.php --quiet' . "\n");
+        fwrite(STDOUT, '  Turn off debug: ./phpmyadmin-report.php -q' . "\n");
+        fwrite(STDOUT, '  Custom config: ./phpmyadmin-report.php --config /home/user/report-config.conf' . "\n");
+        fwrite(STDOUT, '  Custom output: ./phpmyadmin-report.php --output /home/user/report-output.md' . "\n");
+        fwrite(STDOUT, '  Store json data: ./phpmyadmin-report.php --output-json /home/user/report-data.json' . "\n");
+        fwrite(STDOUT, '  Last month: ./phpmyadmin-report.php --last-month' . "\n");
+        fwrite(STDOUT, '  Current month: ./phpmyadmin-report.php --current-month' . "\n");
+        fwrite(STDOUT, '  Current month (by week): ./phpmyadmin-report.sh --current-month --by-week' . "\n");
+        fwrite(STDOUT, '  Next month: ./phpmyadmin-report.php --next-month' . "\n");
+        fwrite(STDOUT, '  Specific month: ./phpmyadmin-report.php --month October' . "\n");
+        fwrite(STDOUT, '  Custom dates: ./phpmyadmin-report.php --start-date=2021-05-03 --end-date=2021-06-27' . "\n");
+        exit(0);
+    }
+
+    private function checkExt(string $extName): void
+    {
+        if (extension_loaded($extName)) {
+            return;
+        }
+
+        $this->quitError($extName . ' could not be found');
+    }
+
+    private function quitError(string $message, int $exitCode = 1): void
+    {
+        fwrite(STDERR, "\033[0;31m[ERROR] " . $message . "\033[0m" . "\n");
+        exit($exitCode);
+    }
+
+    private function logDebug(string $message): void
+    {
+        if ($this->quietMode) {
+            return;
+        }
+
+        fwrite(STDERR, "\033[1;35m[DEBUG] " . $message . "\033[0m" . "\n");
+    }
+
+    private function logInfo(string $message): void
+    {
+        if ($this->quietMode) {
+            return;
+        }
+
+        fwrite(STDERR, "\033[1;35m[INFO] ${message}\033[0m" . "\n");
+    }
+
+    private function checkExtensions(): void
+    {
+        $this->checkExt('curl');
+    }
+
+    private function detectCheckConfig(): void
+    {
+        if (file_exists($this->configFile)) {
+            return;
+        }
+
+        $this->quitError('Missing config file at: ' . $this->configFile);
+    }
+
+    private function readConfig(): void
+    {
+        $config = parse_ini_file(
+            $this->configFile,
+            true,
+            INI_SCANNER_NORMAL
+        );
+
+        foreach ($config as $configBlockName => $configBlock) {
+            $this->processConfigBlock($configBlock, $configBlockName);
+        }
+    }
+
+    private function getGitLabHost(?string $host): string
+    {
+        if ($host === null || $host === '') {
+            return 'gitlab.com';
+        }
+
+        return $host;
+    }
+
+    private function processConfigBlock(array $configBlockIn, string $configBlockName): void
+    {
+        $typeOrName = $configBlockIn['type'] ?? $configBlockName ?? '?';
+
+        if ($typeOrName === 'gitlab') {
+            $this->processGitLab($configBlockIn);
+        } elseif ($typeOrName === 'github') {
+            $this->processGitHub($configBlockIn);
+        }
+    }
+
+    private function processGitLab(array $configBlockIn): void
+    {
+        $this->logDebug('Processing GitLab projects...');
+
+        $host = $this->getGitLabHost($configBlockIn['host'] ?? null);
+
+        foreach (self::PROJECTS[$host] ?? [] as $projectSlug) {
+            $this->processGitLabProject($configBlockIn, $projectSlug);
+        }
+
+        $this->logDebug('Processing GitLab projects done.');
+    }
+
+    private function processGitHub(array $configBlockIn): void
+    {
+        $this->logDebug('Processing GitHub projects...');
+
+        $host = 'github.com';
+        foreach (self::PROJECTS[$host] as $projectSlug) {
+            $this->processGitHubProject($configBlockIn, $projectSlug);
+        }
+
+        $this->logDebug('Processing GitHub projects done.');
+    }
+
+    private function callApi(string $url, array $requestHeaders): array
+    {
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_USERAGENT, 'phpMyAdmin/reporting-script');
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders);
+
+        $data = curl_exec($ch);
+        if ($data === false) {
+            $this->quitError(
+                'Error while fetching: ' . $url . ' (' . curl_error($ch) . ')'
+            );
+        }
+
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        curl_close($ch);
+
+        if (! in_array($httpCode, [200, 201, 202])) {
+            $this->quitError(
+                'Error code (' . $httpCode . ') from: ' . $url . ' -> ' . $data
+            );
+        }
+
+        $jsonData = json_decode($data, true);
+        if (! is_array($jsonData)) {
+            $this->quitError(
+                'Error data from: ' . $url . ' -> ' . $data
+            );
+        }
+
+        return $jsonData;
+    }
+
+    private function callGitHubApi(array $configBlockIn, string $path): array
+    {
+        $token = $configBlockIn['token'];
+
+        $data = $this->callApi(
+            'https://api.github.com/' . $path,
+            [
+                'Authorization: token ' . $token,
+                'Accept: application/vnd.github+json',
+            ]
+        );
+
+        if ($data['message'] ?? false) {
+            $this->quitError('GitHub API (' . $path . ') error: ' . $data['message']);
+        }
+
+        return $data;
+    }
+
+    private function callGitLabApi(array $configBlockIn, string $path): array
+    {
+        $token = $configBlockIn['token'];
+        $host = $configBlockIn['host'];
+
+        $data = $this->callApi('https://' . $host . '/api/' . $path, ['Authorization: Bearer ' . $token]);
+
+        if ($data['error'] ?? false) {
+            $this->quitError('GitLab (' . $host . ') API (' . $path . ') error (' . $data['error'] . ') : ' . $data['error_description']);
+        }
+
+        return $data;
+    }
+
+    private function processGitLabProject(array $configBlockIn, string $projectSlug): void
+    {
+        $projectSlugUrl = urlencode($projectSlug);
+        $authorEmail = $configBlockIn['authorEmail'];
+        $this->logDebug('Processing GitLab project: ' . $projectSlug);
+        $startDate = $this->startDate->format('Y-m-d\TH:i:sp');
+        $endDate = $this->endDate->format('Y-m-d\TH:i:sp');
+        $commits = $this->callGitLabApi(
+            $configBlockIn,
+            "v4/projects/${projectSlugUrl}/repository/commits?since=${startDate}&until=${endDate}"
+        );
+
+        $commits = array_filter($commits, static function (array $commit) use ($authorEmail): bool {
+            return $commit['author_email'] === $authorEmail;
+        });
+        $this->gitLabCommitsToStorage($commits, $projectSlug);
+    }
+
+    private function processGitHubProject(array $configBlockIn, string $projectSlug): void
+    {
+        $username = $configBlockIn['user'];
+        $this->logDebug('Processing GitHub project: ' . $projectSlug);
+        $startDate = $this->startDate->format('Y-m-d\TH:i:sp');
+        $endDate = $this->endDate->format('Y-m-d\TH:i:sp');
+        $commits = $this->callGitHubApi(
+            $configBlockIn,
+            "repos/${projectSlug}/commits?author=${username}&per_page=100&since=${startDate}&until=${endDate}"
+        );
+
+        $this->gitHubCommitsToStorage($commits, $projectSlug);
+
+        $issues = $this->callGitHubApi(
+            $configBlockIn,
+            "repos/${projectSlug}/issues?assignee=${username}&per_page=100&since=${startDate}&sort=updated&direction=asc&state=closed"
+        );
+
+        $this->gitHubIssuesToStorage($issues, $projectSlug);
+    }
+
+    private function gitHubIssuesToStorage(array $issues, string $projectSlug): void
+    {
+        $issues = array_filter($issues, static function (array $issue): bool {
+            return isset($issue['pull_request']) === false;
+        });
+
+        $issues = array_filter($issues, function (array $issue): bool {
+            $cat = new DateTimeImmutable($issue['closed_at']);
+
+            return $cat >= $this->startDate && $cat <= $this->endDate;
+        });
+
+        $this->issuesStorage[] = [
+            'slug' => $projectSlug,
+            'type' => 'GitHub',
+            'issues' => array_map(static function (array $issue): array {
+                return [
+                    'number' => $issue['number'],
+                    'title' => $issue['title'],
+                    'html_url' => $issue['html_url'],
+                    'closed_at' => new DateTimeImmutable($issue['closed_at']),
+                ];
+            }, $issues),
+        ];
+    }
+
+    private function gitHubCommitsToStorage(array $commits, string $projectSlug): void
+    {
+        $this->commitsStorage[] = [
+            'slug' => $projectSlug,
+            'type' => 'GitHub',
+            'commits' => array_map(static function (array $commit): array {
+                return [
+                    'sha' => $commit['sha'],
+                    'message' => explode("\n", $commit['commit']['message'], 2)[0] ?? $commit['commit']['message'],
+                    'html_url' => $commit['html_url'],
+                    'cdate' => new DateTimeImmutable($commit['commit']['committer']['date']),
+                ];
+            }, $commits),
+        ];
+    }
+
+    private function gitLabCommitsToStorage(array $commits, string $projectSlug): void
+    {
+        $this->commitsStorage[] = [
+            'slug' => $projectSlug,
+            'type' => 'GitLab',
+            'commits' => array_map(static function (array $commit): array {
+                return [
+                    'sha' => $commit['id'],
+                    'message' => $commit['title'],
+                    'html_url' => $commit['web_url'],
+                    'cdate' => new DateTimeImmutable($commit['committed_date']),
+                ];
+            }, $commits),
+        ];
+    }
+
+    private function printFinalData(): void
+    {
+        if ($this->outputJsonData === null) {
+            return;
+        }
+
+        $this->logDebug('Commits data count: ' . count($this->commitsStorage));
+        $this->logDebug('Issues data count: ' . count($this->issuesStorage));
+        file_put_contents(
+            $this->outputJsonData,
+            json_encode([
+                'commits' => $this->commitsStorage,
+                'issues' => $this->issuesStorage,
+            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
+        );
+    }
+
+    private function printData(string $data): void
+    {
+        fwrite($this->outputRender, $data);
+    }
+
+    private function renderFinalData(): void
+    {
+        $this->printData('# Commit list' . "\n");
+
+        foreach ($this->commitsStorage as $storageEntry) {
+            if ($storageEntry['commits'] === []) {
+                continue;
+            }
+
+            // remove some commits base on commit body
+            foreach (self::IGNORE_WORDS as $ignoreWord) {
+                $storageEntry['commits'] = array_filter(
+                    $storageEntry['commits'],
+                    static fn ($commit) => str_contains($commit['message'], $ignoreWord) === false
+                );
+            }
+
+            if ($storageEntry['commits'] === []) {
+                continue;
+            }
+
+            if ($this->outputMode === 'by-week') {
+                $commitsGroup = array_reduce($storageEntry['commits'], static function (array $accumulator, array $element) {
+                    $accumulator[$element['cdate']->format('W')][] = $element;
+
+                    return $accumulator;
+                }, []);
+                ksort($commitsGroup);// newer weeks first
+
+                $this->printData(sprintf("\n## %s (%s)\n", $storageEntry['slug'], $storageEntry['type']));
+
+                foreach ($commitsGroup as $monthNumber => $commits) {
+                    $this->printData(sprintf("\n### Week %s\n\n", $monthNumber));
+                    foreach ($commits as $commit) {
+                        $this->processCommit($commit);
+                    }
+                }
+
+                continue;
+            }
+
+            $this->printData(sprintf("\n## %s (%s)\n\n", $storageEntry['slug'], $storageEntry['type']));
+
+            foreach ($storageEntry['commits'] as $commit) {
+                $this->processCommit($commit);
+            }
+        }
+
+        $this->printData("\n" . '# Handled issues' . "\n");
+
+        foreach ($this->issuesStorage as $storageEntry) {
+            if ($storageEntry['issues'] === []) {
+                continue;
+            }
+
+            if ($this->outputMode === 'by-week') {
+                $issuesGroup = array_reduce($storageEntry['issues'], static function (array $accumulator, array $element) {
+                    $accumulator[$element['closed_at']->format('W')][] = $element;
+
+                    return $accumulator;
+                }, []);
+                ksort($issuesGroup);// newer weeks first
+
+                $this->printData(sprintf("\n## %s (%s)\n", $storageEntry['slug'], $storageEntry['type']));
+
+                foreach ($issuesGroup as $monthNumber => $issues) {
+                    $this->printData(sprintf("\n### Week %s\n\n", $monthNumber));
+                    foreach ($issues as $commit) {
+                        $this->processIssue($commit);
+                    }
+                }
+
+                continue;
+            }
+
+            $this->printData(sprintf("\n## %s (%s)\n\n", $storageEntry['slug'], $storageEntry['type']));
+
+            foreach ($storageEntry['issues'] as $commit) {
+                $this->processIssue($commit);
+            }
+        }
+    }
+
+    private function processIssue(array $issue): void
+    {
+        $this->printData(
+            sprintf(
+                '- [%s - %s](%s)' . "\n",
+                $issue['number'],
+                $issue['title'],
+                $issue['html_url']
+            )
+        );
+    }
+
+    private function processCommit(array $commit): void
+    {
+        $this->printData(
+            sprintf(
+                '- [%s - %s](%s)' . "\n",
+                substr($commit['sha'], 0, 10),
+                $commit['message'],
+                $commit['html_url']
+            )
+        );
+    }
+
+    private function loadDates(): void
+    {
+        $this->logDebug('Using month mode: ' . $this->monthMode);
+
+        // Source: http://databobjr.blogspot.com/2011/06/get-first-and-last-day-of-month-in-bash.html
+
+        // Dates use format: YYYY-MM-DDTHH:MM:SSZ
+
+        if ($this->monthMode === 'last') {
+            // Example (current date: 04/july(07)/2021 23:27): 2021-06-01T00:00:00Z
+            $this->startDate = new DateTimeImmutable('@' . strtotime('first day of last month UTC'));
+            // Example (current date: 04/july(07)/2021 23:27): 2021-06-30T00:00:00Z
+            $this->endDate = new DateTimeImmutable('@' . strtotime('last day of last month UTC'));
+        }
+
+        if ($this->monthMode === 'current') {
+            // Example (current date: 04/july(07)/2021 23:27): 2021-07-01T00:00:00Z
+            $this->startDate = new DateTimeImmutable('@' . strtotime('first day of this month UTC'));
+            // Example (current date: 04/july(07)/2021 23:27): 2021-07-31T00:00:00Z
+            $this->endDate = new DateTimeImmutable('@' . strtotime('last day of this month UTC'));
+        }
+
+        if ($this->monthMode === 'next') {
+            // Example (current date: 04/july(07)/2021 23:27): 2021-08-01T00:00:00Z
+            $this->startDate = new DateTimeImmutable('@' . strtotime('first day of next month UTC'));
+            // Example (current date: 04/july(07)/2021 23:27): 2021-08-31T00:00:00Z
+            $this->endDate = new DateTimeImmutable('@' . strtotime('last day of next month UTC'));
+        }
+
+        $this->startDate = $this->startDate->setTimezone(new DateTimeZone('UTC'));
+        $this->startDate = $this->startDate->setTime(00, 00, 00);
+        $this->endDate = $this->endDate->setTimezone(new DateTimeZone('UTC'));
+        $this->endDate = $this->endDate->setTime(23, 59, 59);
+
+        $this->logDebug('Start date (Y-m-d H:i:s): ' . $this->startDate->format('Y-m-d H:i:s'));
+        $this->logDebug('End date (Y-m-d H:i:s): ' . $this->endDate->format('Y-m-d H:i:s'));
+    }
+}
+
+(new Reports())->run();
diff --git a/phpmyadmin-reports b/phpmyadmin-reports
index 3975e41..102cf33 100755
--- a/phpmyadmin-reports
+++ b/phpmyadmin-reports
@@ -17,7 +17,7 @@ Requirements:
 from configparser import RawConfigParser, NoOptionError, NoSectionError
 import os
 import sys
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from argparse import ArgumentParser, RawDescriptionHelpFormatter
 import dateutil.parser
 try:
@@ -43,6 +43,9 @@ PROJECTS = (
     'phpmyadmin/simple-math',
     'phpmyadmin/localized_docs',
     'phpmyadmin/error-reporting-server',
+    'phpmyadmin/twig-i18n-extension',
+    'phpmyadmin/coding-standard',
+    'phpmyadmin/scripts',
 )
 # Only include commits not present elsewhere from this repository
 PROJECT_EXTRA_COMMITS = (
@@ -264,8 +267,8 @@ def main(params):
         end = args.end_date
         weekly = args.weekly
 
-    start = datetime(start.year, start.month, start.day)
-    end = datetime(end.year, end.month, end.day, 23, 59, 59)
+    start = datetime(start.year, start.month, start.day, tzinfo=timezone.utc)
+    end = datetime(end.year, end.month, end.day, 23, 59, 59, tzinfo=timezone.utc)
 
     issues, commits = get_data(user, token, start, end, weekly)
 
diff --git a/website/daily-composer b/website/daily-composer
index 5008f3d..26a9750 100755
--- a/website/daily-composer
+++ b/website/daily-composer
@@ -12,7 +12,7 @@ set -u
 DIR=~/composer
 
 # Branches we process
-BRANCHES="QA_5_0 QA_4_9 QA_4_8 QA_4_7 master"
+BRANCHES="QA_5_2 QA_4_9 master"
 
 generate_readme() {
     cat > README.rst <<EOT
@@ -34,31 +34,103 @@ EOT
 }
 
 generate_mo() {
-    rm -rf locale
-    ./scripts/generate-mo
-    git add -f locale
+    # TODO: This can be cleaned up and removed when no release exists with ./scripts/
+    if [ -f ./scripts/generate-mo ]; then
+      rm -rf locale
+      ./scripts/generate-mo
+      git add -f locale
+    else
+      rm -rf resources/locale
+      ./bin/generate-mo
+      git add -f resources/locale
+    fi
     if ! git diff --quiet --cached ; then
         git commit -m 'Update mo files'
     fi
 }
 
-line_counts() {
-    if [ ! -x ./scripts/line-counts.sh ] ; then
+generate_css() {
+    # You can not use --production because it creates the error "[BABEL]: Cannot find module 'browserslist'"
+    yarn install --frozen-lockfile --non-interactive --silent
+    if [ -f ./bin/console ]; then
+        git add --force public/themes/*/css/*.css || echo "No css file found, not adding to git repository"
+        git add --force public/themes/*/css/*.css.map || echo "No css.map files found, not adding to git repository"
+    else
+        git add --force themes/*/css/*.css || echo "No css file found, not adding to git repository"
+        git add --force themes/*/css/*.css.map || echo "No css.map files found, not adding to git repository"
+    fi
+    if ! git diff --quiet --cached ; then
+        git commit -m 'Update generated CSS files'
+    fi
+}
+
+generate_js() {
+    # You can not use --production because it creates the error "[BABEL]: Cannot find module 'browserslist'"
+    yarn install --frozen-lockfile --non-interactive --silent
+    if [ -f ./bin/console ]; then
+        git add --force public/js/dist/ || echo "No JS files found, not adding to git repository"
+    else
+        git add --force js/dist/ || echo "No JS files found, not adding to git repository"
+    fi
+    if ! git diff --quiet --cached ; then
+        git commit -m 'Update generated JS files'
+    fi
+}
+
+# Keep in sync with phpmyadmin/phpmyadmin:scripts/create-release.sh
+fetchReleaseFromFile() {
+    php -r "define('VERSION_SUFFIX', ''); require_once('libraries/classes/Version.php'); echo \PhpMyAdmin\Version::VERSION;"
+}
+
+set_version_in_composer() {
+    if [ ! -f libraries/classes/Version.php ]; then
+        echo "The version file does not exist for this version"
         return
     fi
-    ./scripts/line-counts.sh
-    git add -f js/line_counts.php
+
+    # The reason the version is added to the composer.json file is because of:
+    # - https://github.com/phpmyadmin/phpmyadmin/issues/17570
+    # - https://github.com/Roave/SecurityAdvisories/issues/76
+
+    echo "Detected version: $(fetchReleaseFromFile)"
+    composer config version "$(fetchReleaseFromFile)"
+    echo "Saved version: $(composer config version)"
+    git add composer.json
     if ! git diff --quiet --cached ; then
-        git commit -m 'Updated line counts' js/line_counts.php
+        git commit -m 'Set version in composer.json' -m "Version: $(composer config version)"
     fi
 }
 
-generate_css() {
-    yarn install --non-interactive --silent
-    git add --force themes/*/css/*.css
-    git add --force themes/*/css/*.css.map
+reset_not_allowed_to_change_source_files() {
+    # Source: https://stackoverflow.com/a/68019875/5155484
+
+    if [ -f yarn-error.log ]; then
+        rm -v yarn-error.log
+    fi
+
+    # Source: https://stackoverflow.com/a/2196755/5155484
+    echo "Cleaning ignored files"
+    git clean -fdX
+
+    if [ -f ./bin/console ]; then
+        CHANGED_FILES="$(git diff phpmyadmin/$branch..$newbranch --name-only -- './*' ':!README.rst' ':!public/js/dist/*.js' ':!public/js/dist/*.js.map' ':!resources/locale/*.mo' ':!public/themes/*.css' ':!public/themes/*.css.map')"
+    else
+        CHANGED_FILES="$(git diff phpmyadmin/$branch..$newbranch --name-only -- './*' ':!README.rst' ':!js/dist/*.js' ':!js/dist/*.js.map' ':!locale/*.mo' ':!themes/*.css' ':!themes/*.css.map')"
+    fi
+
+    if [ -z "$CHANGED_FILES" ]; then
+        echo "No files did change"
+        return
+    fi
+
+    echo "Files changed: $CHANGED_FILES"
+
+    echo "$CHANGED_FILES" | xargs git checkout phpmyadmin/$branch --theirs
+    echo "$CHANGED_FILES" | xargs git add --force
+    unset CHANGED_FILES
+
     if ! git diff --quiet --cached ; then
-        git commit -m 'Update generated CSS files'
+        git commit -m 'Reset changed source files'
     fi
 }
 
@@ -76,6 +148,9 @@ git remote update phpmyadmin
 # Fetch tags (we don't want them as tags)
 git fetch phpmyadmin '+refs/tags/*:refs/remotes/phpmyadmin/tags/*'
 
+# Destroy local changes
+git reset --hard
+
 # Process branches
 for branch in $BRANCHES ; do
     if [ $branch = master ] ; then
@@ -96,18 +171,31 @@ for branch in $BRANCHES ; do
         git checkout -b $newbranch phpmyadmin/$branch
     fi
 
+    # Destroy any possible changes
+    git reset --hard
+    # Force an update just in case
+    git pull --ff --rebase=false
+
+    # Reset content that could have changed by mistake
+    reset_not_allowed_to_change_source_files
+
     # Update content
     generate_readme
     generate_mo
-    line_counts
     generate_css
+    generate_js
+    set_version_in_composer
 
     # Push
     git push origin $newbranch
 done
 
 # Process tags
-for tag in `git branch -r | grep 'phpmyadmin/tags/RELEASE_[45]_[7890]_[0-9]\+\(_[0-9]\)\?$'` ; do
+
+# We don't take tags prior to 4.7.0, since that's when we started distributing through Composer.
+# This grep string matches 4.7-9 or anything higher than 5.
+# We also do not match on any release candidate or development release.
+for tag in $(git branch -r | grep 'phpmyadmin/tags/\(RELEASE_[4]_[7-9]_[0-9]\+\(_[0-9]\)\?$\)\|\(RELEASE_\([^1-4]\|[0-9]\{2,\}\)\_[0-9]\+_[0-9]\+\(_[0-9]\)\?$\)') ; do
     newtag=`echo $tag | sed -e 's@.*RELEASE_@@' -e 's/_/./g'`
     if git rev-parse --verify --quiet $newtag ; then
         continue
@@ -120,8 +208,9 @@ for tag in `git branch -r | grep 'phpmyadmin/tags/RELEASE_[45]_[7890]_[0-9]\+\(_
     # Update content
     generate_readme
     generate_mo
-    line_counts
     generate_css
+    generate_js
+    set_version_in_composer
 
     # Create tag
     git tag -a -m "Composer tag for $tag" $newtag
@@ -130,6 +219,6 @@ for tag in `git branch -r | grep 'phpmyadmin/tags/RELEASE_[45]_[7890]_[0-9]\+\(_
     git checkout master
     git branch -D tmp
 
-    # Push tags
-    git push --tags origin
+    # Push the tag
+    git push origin $newtag
 done
diff --git a/website/daily-snapshots b/website/daily-snapshots
index 05f32c6..e411b77 100755
--- a/website/daily-snapshots
+++ b/website/daily-snapshots
@@ -1,10 +1,13 @@
 #!/bin/sh
 # Script to generate daily snapshots
-set -e
+set -ex
 
 # phpMyAdmin working copy
 cd ~/phpmyadmin
 
+# Delete any local changes
+git reset --hard
+
 # Update to have latest create-release.sh
 git pull -q
 
@@ -12,9 +15,23 @@ git pull -q
 rm -rf release
 
 # Create snapshots
-./scripts/create-release.sh --daily 4.9+snapshot QA_4_9
-./scripts/create-release.sh --daily 5.0+snapshot QA_5_0
-./scripts/create-release.sh --daily 5.1+snapshot master
+# Change branch to be sure to have a working release script
+git checkout QA_5_2
+# Destroy any possible local changes
+git reset --hard
+# Force an update
+git pull --ff-only
+#TODO: When changing to QA_6_0, update from ./scripts/ to ./bin/internal/
+./scripts/create-release.sh --daily 5.2+snapshot QA_5_2
+# Destroy any possible local changes
+git reset --hard
+git checkout -
+git checkout master
+# Destroy any possible local changes
+git reset --hard
+# Force an update
+git pull --ff-only
+./bin/internal/create-release.sh --daily 6.0+snapshot master
 
 # Storage directory
 mkdir -p /mnt/storage/files/snapshots/
@@ -26,11 +43,7 @@ rm -f /mnt/storage/files/snapshots/*
 mv release/*.* /mnt/storage/files/snapshots/
 
 # Rsync to CDN
-rsync -va /mnt/storage/files/ cdn:/www/
+rsync --delete-before -va /mnt/storage/files/ cdn:/www/
 
 # trigger website scan
-cd /home/builder/website/
-./manage.py import_files
-
-# Trigger docker build for edge-4.9
-./manage.py docker_trigger --tag edge-4.9
+sudo -u builder /home/builder/website/manage.py import_files
diff --git a/website/generate-missing-checksums b/website/generate-missing-checksums
index d1c27e2..f32da58 100755
--- a/website/generate-missing-checksums
+++ b/website/generate-missing-checksums
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# Generates missing checksums for donwload files
+# Generates missing checksums for download files
 
 cd /mnt/storage/files
 
diff --git a/website/pma-planet b/website/pma-planet
deleted file mode 100755
index 48344a2..0000000
--- a/website/pma-planet
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-# Script to generate Planet phpMyAdmin content
-
-set -e
-
-cd /home/builder/planet/
-git pull -q
-planet planet.ini
diff --git a/website/sync-files-cdn b/website/sync-files-cdn
index bb433bc..eb4caec 100755
--- a/website/sync-files-cdn
+++ b/website/sync-files-cdn
@@ -13,7 +13,7 @@
 #
 
 # rsync to CDN
-rsync -va /mnt/storage/files/ cdn:/www/
+rsync --delete-before -va /mnt/storage/files/ cdn:/www/
 
 # trigger website scan
 cd /home/builder/website/
