Skip to content

Instantly share code, notes, and snippets.

@mlocati
Last active January 24, 2026 19:52
Show Gist options
  • Select an option

  • Save mlocati/5db7bb36b4c3ac7676a4ace97b69ab46 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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
#!/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