Last active
January 24, 2026 19:52
-
-
Save mlocati/5db7bb36b4c3ac7676a4ace97b69ab46 to your computer and use it in GitHub Desktop.
Scripts for my "Deploy with git push" article https://mlocati.github.io/articles/deploy-with-git-push.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # A post-receive git hook to publish a specific branch to a working tree, | |
| # optionally updating Composer dependencies and/or ConcreteCMS. | |
| # | |
| # Invoke this file from hooks/post-receive. | |
| # For example: | |
| # | |
| # #!/bin/sh | |
| # /path/to/this-file /path/to/repo.git /path/to/worktree --branch main --composer --concrete --concrete-languages 'de_DE fr_FR it_IT' | |
| # | |
| # Requirements: | |
| # - run visudo and add these lines at the end | |
| # git ALL=(www-data) NOPASSWD: /usr/bin/git, /usr/local/bin/composer, /usr/local/bin/update-concrete | |
| # | |
| # Copyright Michele Locati <michele@locati.it> - 2026 | |
| # License: MIT - https://opensource.org/license/mit | |
| # Source: https://gist.github.com/mlocati/5db7bb36b4c3ac7676a4ace97b69ab46 | |
| set -o nounset | |
| set -o errexit | |
| safeEcho() { | |
| printf -- '%s\n' "${1:-}" || : | |
| } | |
| exitWithError() { | |
| message="${1:-}" | |
| messageLength=${#message} | |
| lineLength=$(expr "$messageLength" + 8) | |
| fullLineHash= | |
| i=1 | |
| while [ $i -le $lineLength ]; do | |
| fullLineHash="${fullLineHash}#" | |
| i=$(expr $i + 1) | |
| done | |
| messageLengthSpaces= | |
| i=1 | |
| while [ $i -le $messageLength ]; do | |
| messageLengthSpaces="${messageLengthSpaces} " | |
| i=$(expr $i + 1) | |
| done | |
| printf '\n%s\n' "$fullLineHash" || : | |
| printf '## %s ##\n' "$messageLengthSpaces" || : | |
| printf '## %s ##\n' "$message" || : | |
| printf '## %s ##\n' "$messageLengthSpaces" || : | |
| printf '%s\n\n' "$fullLineHash" || : | |
| cat <<'EOT' | |
| ____ _ _ _ _ | |
| / __ \| | | \ | | | | | |
| | | | | |__ | \| | ___ | | | |
| | | | | '_ \ | . ` |/ _ \| | | |
| | |__| | | | | | |\ | (_) |_| | |
| \____/|_| |_| |_| \_|\___/(_) | |
| (pushToPublishHookError) | |
| EOT | |
| exit 1 | |
| } | |
| showSyntax() { | |
| safeEcho "Syntax: $0 [options] git-dir worktree-dir" | |
| safeEcho | |
| safeEcho 'Options:' | |
| safeEcho ' -h, --help Show this help message and exit' | |
| safeEcho ' --branch BRANCH_NAME Git branch to be published (default: main)' | |
| safeEcho ' --composer Install Composer dependencies from composer.lock' | |
| safeEcho ' --concrete Update ConcreteCMS' | |
| safeEcho ' --concrete-languages LIST List of ConcreteCMS language codes (e.g., 'de_DE de_CH fr_FR it_IT')' | |
| } | |
| safeEcho "Hello $(whoami)! I'm the git post-receive hook $0 running on $(hostname)" | |
| safeEcho | |
| if [ $# -eq 0 ]; then | |
| showSyntax | |
| exit 1 | |
| fi | |
| for arg in "$@"; do | |
| case "$arg" in | |
| -h|--help) | |
| showSyntax | |
| exit 0 | |
| ;; | |
| esac | |
| done | |
| gitDir= | |
| workTree= | |
| branchName=main | |
| updateComposer=0 | |
| updateConcrete=0 | |
| concreteLanguages= | |
| skipPrefix= | |
| while [ $# -gt 0 ]; do | |
| case "$skipPrefix$1" in | |
| --branch) | |
| shift | |
| if [ -z "${1:-}" ]; then | |
| exitWithError 'Error: --branch requires an argument' | |
| fi | |
| branchName="$1" | |
| ;; | |
| --composer) | |
| updateComposer=1 | |
| ;; | |
| --concrete) | |
| updateConcrete=1 | |
| ;; | |
| --concrete-languages) | |
| shift | |
| if [ -z "${1:-}" ]; then | |
| exitWithError '--concrete-languages requires an argument' | |
| fi | |
| concreteLanguages="$1" | |
| ;; | |
| --) | |
| skipPrefix=SKIP | |
| ;; | |
| -*) | |
| exitWithError "Unknown option: $1" | |
| ;; | |
| *) | |
| # Gli argomenti non opzioni | |
| if [ -z "$gitDir" ]; then | |
| gitDir="${1%/}" | |
| if [ -z "$gitDir" ] || ! [ -d "$gitDir" ]; then | |
| exitWithError "The specified git directory '$gitDir' does not exist." | |
| fi | |
| if [ ! -f "$gitDir/HEAD" ]; then | |
| exitWithError "Not a git bare repository (no HEAD): '$gitDir'" | |
| fi | |
| if [ ! -f "$gitDir/config" ]; then | |
| exitWithError "Not a git bare repository (no config): '$gitDir'" | |
| fi | |
| if ! git --git-dir="$gitDir" rev-parse --is-bare-repository 2>/dev/null | grep -qx true; then | |
| exitWithError "Not a git bare repository: '$gitDir'" | |
| fi | |
| elif [ -z "$workTree" ]; then | |
| workTree="${1%/}" | |
| if [ -z "$workTree" ] || ! [ -d "$workTree" ]; then | |
| exitWithError "The specified work tree directory '$workTree' does not exist." | |
| fi | |
| else | |
| exitWithError "Too many arguments: $1" | |
| fi | |
| ;; | |
| esac | |
| shift | |
| done | |
| if [ -z "$gitDir" ] || [ -z "$workTree" ]; then | |
| exitWithError 'git directory and work tree directory must be specified' | |
| fi | |
| if ! cd -- "$workTree" >/dev/null; then | |
| exitWithError "Failed to enter work tree directory '$workTree'" | |
| fi | |
| safeEcho "### Publishing branch '$branchName' from '$gitDir' to '$workTree'" | |
| if ! sudo -n -u www-data -- git --git-dir="$gitDir" --work-tree="$workTree" checkout --force "$branchName"; then | |
| exitWithError 'git checkout failed' | |
| fi | |
| safeEcho | |
| if [ $updateComposer -eq 1 ]; then | |
| safeEcho '### Running composer install' | |
| if [ -f ./composer.json ] && [ -f ./composer.lock ]; then | |
| if ! sudo -n -u www-data -- /usr/local/bin/composer --no-ansi --no-interaction --no-cache --prefer-dist --no-dev --optimize-autoloader --no-progress install; then | |
| exitWithError 'composer install failed' | |
| fi | |
| else | |
| safeEcho 'Skipping (composer.json and/or composer.lock files not found)' | |
| fi | |
| safeEcho | |
| fi | |
| if [ $updateConcrete -eq 1 ]; then | |
| safeEcho '### Updating ConcreteCMS' | |
| concreteDirBase= | |
| for dir in web public; do | |
| if [ -f "./$dir/concrete/bin/concrete5" ]; then | |
| concreteDirBase="./$dir" | |
| break | |
| fi | |
| done | |
| if [ -z "$concreteDirBase" ]; then | |
| safeEcho 'Skipping (failed to find the concrete directory)' | |
| elif ! [ -f "${concreteDirBase}/application/config/database.php" ]; then | |
| safeEcho 'Skipping (ConcreteCMS not yet installed)' | |
| else | |
| if ! sudo -n -u www-data /usr/local/bin/update-concrete "$concreteDirBase" $concreteLanguages; then | |
| exitWithError 'Failed to update ConcreteCMS' | |
| fi | |
| fi | |
| fi | |
| cat <<'EOT' | |
| _ | |
| /(| | |
| ( : | |
| __\ \ _____ | |
| (____) `| | |
| (____)| | | |
| (____).__| | |
| (___)__.|_____ | |
| EOT |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # First argument (required): path to the directory that contains the ConcreteCMS installation | |
| # Subsequent arguments (optional): language codes to install/update translations for (e.g. 'fr_FR', 'de_DE', etc.) | |
| # | |
| # Copyright Michele Locati <michele@locati.it> - 2026 | |
| # License: MIT - https://opensource.org/license/mit | |
| # Source: https://gist.github.com/mlocati/5db7bb36b4c3ac7676a4ace97b69ab46 | |
| set -o nounset | |
| set -o errexit | |
| concreteBin= | |
| resetMaintenanceMode=0 | |
| safeEcho() { | |
| printf -- '%s\n' "${1:-}" || : | |
| } | |
| cleanup() { | |
| if [ $resetMaintenanceMode -ne 1 ] || [ -z "$concreteBin" ]; then | |
| return | |
| fi | |
| safeEcho '# Turning off maintenance mode' | |
| if "$concreteBin" --no-ansi --no-interaction c5:config -g set concrete.maintenance_mode false; then | |
| resetMaintenanceMode=0 | |
| safeEcho 'Maintenance mode turned off.' | |
| else | |
| safeEcho 'Failed to turn off maintenance mode!' | |
| fi | |
| safeEcho | |
| } | |
| died() { | |
| cleanup | |
| exit 1 | |
| } | |
| exitWithError() { | |
| safeEcho "$1" | |
| exit 1 | |
| } | |
| buildInstallLanguageArgs() { | |
| oldIFS=$IFS | |
| IFS=' ' | |
| args= | |
| for lang in "$@"; do | |
| args="$args --add $lang" | |
| done | |
| IFS=$oldIFS | |
| echo $args | |
| } | |
| trap '' HUP | |
| trap cleanup EXIT | |
| trap 'exit 1' INT TERM | |
| if [ "$(id -u 2>/dev/null)" -eq 0 ]; then | |
| safeEcho "$(basename -- "$0") must not be run as root." | |
| exit 1 | |
| fi | |
| if [ $# -lt 1 ]; then | |
| safeEcho "$(basename -- "$0") requires at least one argument (the path to the directory that contains the ConcreteCMS installation)." | |
| exit 1 | |
| fi | |
| dirBase="$1" | |
| if ! [ -d "$dirBase" ]; then | |
| safeEcho "The specified directory '$dirBase' does not exist or is not a directory." | |
| exit 1 | |
| fi | |
| concreteBin="$dirBase/concrete/bin/concrete5" | |
| if ! [ -f "$concreteBin" ]; then | |
| safeEcho "The specified directory '$dirBase' does not appear to contain a ConcreteCMS installation (concrete/bin/concrete5 not found in it)." | |
| exit 1 | |
| fi | |
| shift | |
| installLanguageArgs=$(buildInstallLanguageArgs "$@") | |
| safeEcho '# Turning on maintenance mode' | |
| "$concreteBin" --no-ansi --no-interaction --verbose c5:config -g set concrete.maintenance_mode true || exitWithError 'Failed to enable maintenance mode!' | |
| resetMaintenanceMode=1 | |
| safeEcho 'Maintenance mode turned on.' | |
| safeEcho | |
| safeEcho '# Updating ConcreteCMS' | |
| "$concreteBin" --no-ansi --no-interaction --verbose c5:update || exitWithError 'Failed to update ConcreteCMS!' | |
| safeEcho 'ConcreteCMS updated.' | |
| safeEcho | |
| safeEcho '# Updating packages' | |
| "$concreteBin" --no-ansi --no-interaction --verbose c5:package:update --all || exitWithError 'Failed to update packages!' | |
| safeEcho | |
| safeEcho "# Installing/updating translations ($( [ -z "$installLanguageArgs" ] && echo 'no args' || echo "args: $installLanguageArgs" ))" | |
| if ! "$concreteBin" --no-ansi --no-interaction --verbose c5:install-language --update --core --packages $installLanguageArgs; then | |
| safeEcho 'Failed to install/update translations!' | |
| fi | |
| safeEcho | |
| safeEcho '# Refreshing Doctrine entities' | |
| "$concreteBin" --no-ansi --no-interaction --verbose c5:entities:refresh || exitWithError 'Failed to refresh Doctrine entities!' | |
| safeEcho | |
| safeEcho '# Clearing ConcreteCMS cache' | |
| "$concreteBin" --no-ansi --no-interaction --verbose c5:clear-cache || exitWithError 'Failed to clear ConcreteCMS cache!' | |
| safeEcho |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment