Last active
January 23, 2026 16:42
-
-
Save mlocati/8b3df1cf72d110cc38ed3ffc70fa0bbd to your computer and use it in GitHub Desktop.
A git post-receive hook to publish/deploy websites
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/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