Skip to content

Instantly share code, notes, and snippets.

@mlocati
Last active January 23, 2026 16:42
Show Gist options
  • Select an option

  • Save mlocati/8b3df1cf72d110cc38ed3ffc70fa0bbd to your computer and use it in GitHub Desktop.

Select an option

Save mlocati/8b3df1cf72d110cc38ed3ffc70fa0bbd to your computer and use it in GitHub Desktop.
A git post-receive hook to publish/deploy websites
#!/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/8b3df1cf72d110cc38ed3ffc70fa0bbd
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment