Last active
March 1, 2026 17:08
-
-
Save ROCKTAKEY/5cd247f981202b9e31699b309044e61b to your computer and use it in GitHub Desktop.
Mattermost upgrade batch
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| readonly SCRIPT_NAME="$(basename "$0")" | |
| VERSION="" | |
| EDITION="enterprise" | |
| ARCH="amd64" | |
| INSTALL_PATH="/opt" | |
| DOWNLOAD_DIR="/tmp" | |
| SERVICE_NAME="mattermost" | |
| OWNER="" | |
| GROUP="" | |
| SETCAP_ENABLED=0 | |
| DRY_RUN=0 | |
| KEEP_TMP=0 | |
| ARCHIVE_PATH="" | |
| ARCHIVE_WAS_DOWNLOADED=0 | |
| SERVICE_STOPPED=0 | |
| declare -a EXTRA_PRESERVE_PATHS=() | |
| usage() { | |
| cat <<'EOF' | |
| Usage: | |
| mattermost-upgrade.sh --version X.Y.Z [options] | |
| Required: | |
| --version X.Y.Z Mattermost target version (example: 10.11.2) | |
| Options: | |
| --edition enterprise|team Edition to download (default: enterprise) | |
| --arch amd64|arm64 Archive architecture (default: amd64) | |
| --install-path PATH Base install path that contains ./mattermost (default: /opt) | |
| --download-dir PATH Temporary working directory (default: /tmp) | |
| --service NAME systemd service name (default: mattermost) | |
| --owner USER Owner for chown (default: detected from existing binary, fallback: mattermost) | |
| --group GROUP Group for chown (default: detected from existing binary, fallback: mattermost) | |
| --preserve-path REL_PATH Extra relative path under mattermost/ to preserve (repeatable) | |
| --archive PATH Use local archive instead of downloading | |
| --setcap Run setcap on ./mattermost/bin/mattermost after copy | |
| --keep-tmp Keep extracted files and archive in download-dir | |
| --dry-run Print commands without executing | |
| -h, --help Show this help | |
| EOF | |
| } | |
| log() { | |
| printf '[%s] %s\n' "$SCRIPT_NAME" "$*" | |
| } | |
| fail() { | |
| printf '[%s] ERROR: %s\n' "$SCRIPT_NAME" "$*" >&2 | |
| exit 1 | |
| } | |
| need_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || fail "command not found: $1" | |
| } | |
| run_cmd() { | |
| if [[ "$DRY_RUN" -eq 1 ]]; then | |
| printf '+' | |
| for arg in "$@"; do | |
| printf ' %q' "$arg" | |
| done | |
| printf '\n' | |
| return 0 | |
| fi | |
| "$@" | |
| } | |
| declare -a ROOT_PREFIX=() | |
| run_root_cmd() { | |
| run_cmd "${ROOT_PREFIX[@]}" "$@" | |
| } | |
| recover_service_on_error() { | |
| local status=$? | |
| if [[ "$status" -ne 0 && "$SERVICE_STOPPED" -eq 1 ]]; then | |
| log "Upgrade failed. Attempting service recovery: systemctl start $SERVICE_NAME" | |
| if [[ "$DRY_RUN" -eq 1 ]]; then | |
| run_root_cmd systemctl start "$SERVICE_NAME" || true | |
| else | |
| run_root_cmd systemctl start "$SERVICE_NAME" >/dev/null 2>&1 || true | |
| fi | |
| fi | |
| exit "$status" | |
| } | |
| trim_trailing_slash() { | |
| local path="$1" | |
| if [[ "$path" == "/" ]]; then | |
| printf '/\n' | |
| else | |
| printf '%s\n' "${path%/}" | |
| fi | |
| } | |
| normalize_rel_path() { | |
| local rel="$1" | |
| rel="${rel#./}" | |
| rel="${rel%/}" | |
| [[ -n "$rel" ]] || fail "empty preserve path is not allowed" | |
| [[ "$rel" != /* ]] || fail "preserve path must be relative to mattermost/: $1" | |
| [[ "$rel" != *".."* ]] || fail "preserve path must not include '..': $1" | |
| printf '%s\n' "$rel" | |
| } | |
| path_is_preserved() { | |
| local rel="$1" | |
| local keep | |
| for keep in "${PRESERVE_PATHS[@]}"; do | |
| if [[ "$rel" == "$keep" || "$rel" == "$keep/"* || "$keep" == "$rel/"* ]]; then | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| cleanup_current_install() { | |
| local mm_dir="$1" | |
| local entry rel | |
| local -a top_entries=() | |
| local -a client_entries=() | |
| shopt -s nullglob dotglob | |
| for entry in "$mm_dir"/*; do | |
| rel="${entry#"$mm_dir"/}" | |
| if path_is_preserved "$rel"; then | |
| continue | |
| fi | |
| top_entries+=("$entry") | |
| done | |
| if [[ -d "$mm_dir/client" ]]; then | |
| for entry in "$mm_dir/client"/*; do | |
| rel="client/${entry#"$mm_dir/client/"}" | |
| if path_is_preserved "$rel"; then | |
| continue | |
| fi | |
| client_entries+=("$entry") | |
| done | |
| fi | |
| shopt -u nullglob dotglob | |
| if [[ "${#top_entries[@]}" -gt 0 ]]; then | |
| log "Removing stale top-level files/directories from $mm_dir" | |
| for entry in "${top_entries[@]}"; do | |
| run_root_cmd rm -rf -- "$entry" | |
| done | |
| fi | |
| if [[ "${#client_entries[@]}" -gt 0 ]]; then | |
| log "Removing stale client files/directories from $mm_dir/client" | |
| for entry in "${client_entries[@]}"; do | |
| run_root_cmd rm -rf -- "$entry" | |
| done | |
| fi | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --version) | |
| VERSION="${2:-}" | |
| shift 2 | |
| ;; | |
| --edition) | |
| EDITION="${2:-}" | |
| shift 2 | |
| ;; | |
| --arch) | |
| ARCH="${2:-}" | |
| shift 2 | |
| ;; | |
| --install-path) | |
| INSTALL_PATH="${2:-}" | |
| shift 2 | |
| ;; | |
| --download-dir) | |
| DOWNLOAD_DIR="${2:-}" | |
| shift 2 | |
| ;; | |
| --service) | |
| SERVICE_NAME="${2:-}" | |
| shift 2 | |
| ;; | |
| --owner) | |
| OWNER="${2:-}" | |
| shift 2 | |
| ;; | |
| --group) | |
| GROUP="${2:-}" | |
| shift 2 | |
| ;; | |
| --preserve-path) | |
| EXTRA_PRESERVE_PATHS+=("$(normalize_rel_path "${2:-}")") | |
| shift 2 | |
| ;; | |
| --archive) | |
| ARCHIVE_PATH="${2:-}" | |
| shift 2 | |
| ;; | |
| --setcap) | |
| SETCAP_ENABLED=1 | |
| shift | |
| ;; | |
| --keep-tmp) | |
| KEEP_TMP=1 | |
| shift | |
| ;; | |
| --dry-run) | |
| DRY_RUN=1 | |
| shift | |
| ;; | |
| -h | --help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| fail "unknown option: $1" | |
| ;; | |
| esac | |
| done | |
| [[ -n "$VERSION" ]] || { | |
| usage | |
| fail "--version is required" | |
| } | |
| case "$EDITION" in | |
| enterprise | team) ;; | |
| *) fail "--edition must be either 'enterprise' or 'team'" ;; | |
| esac | |
| case "$ARCH" in | |
| amd64 | arm64) ;; | |
| *) fail "--arch must be either 'amd64' or 'arm64'" ;; | |
| esac | |
| need_cmd tar | |
| need_cmd cp | |
| need_cmd rm | |
| need_cmd chown | |
| need_cmd systemctl | |
| need_cmd stat | |
| if [[ "$SETCAP_ENABLED" -eq 1 ]]; then | |
| need_cmd setcap | |
| fi | |
| if [[ "$(id -u)" -ne 0 ]]; then | |
| need_cmd sudo | |
| ROOT_PREFIX=(sudo) | |
| fi | |
| INSTALL_PATH="$(trim_trailing_slash "$INSTALL_PATH")" | |
| DOWNLOAD_DIR="$(trim_trailing_slash "$DOWNLOAD_DIR")" | |
| readonly MATTERMOST_DIR="$INSTALL_PATH/mattermost" | |
| readonly UPGRADE_DIR="$DOWNLOAD_DIR/mattermost-upgrade" | |
| [[ -d "$MATTERMOST_DIR" ]] || fail "install directory not found: $MATTERMOST_DIR" | |
| [[ "$MATTERMOST_DIR" == */mattermost ]] || fail "invalid install directory: $MATTERMOST_DIR" | |
| if [[ "$EDITION" == "enterprise" ]]; then | |
| ARCHIVE_NAME="mattermost-${VERSION}-linux-${ARCH}.tar.gz" | |
| else | |
| ARCHIVE_NAME="mattermost-team-${VERSION}-linux-${ARCH}.tar.gz" | |
| fi | |
| if [[ -z "$ARCHIVE_PATH" ]]; then | |
| ARCHIVE_PATH="$DOWNLOAD_DIR/$ARCHIVE_NAME" | |
| DOWNLOAD_URL="https://releases.mattermost.com/${VERSION}/${ARCHIVE_NAME}" | |
| ARCHIVE_WAS_DOWNLOADED=1 | |
| fi | |
| if [[ ! -f "$MATTERMOST_DIR/bin/mattermost" ]]; then | |
| fail "existing binary not found: $MATTERMOST_DIR/bin/mattermost" | |
| fi | |
| CURRENT_OWNER_GROUP="$(stat -c '%U:%G' "$MATTERMOST_DIR/bin/mattermost")" | |
| if [[ -z "$OWNER" ]]; then | |
| OWNER="${CURRENT_OWNER_GROUP%%:*}" | |
| [[ -n "$OWNER" ]] || OWNER="mattermost" | |
| fi | |
| if [[ -z "$GROUP" ]]; then | |
| GROUP="${CURRENT_OWNER_GROUP##*:}" | |
| [[ -n "$GROUP" ]] || GROUP="mattermost" | |
| fi | |
| declare -a PRESERVE_PATHS=( | |
| "client" | |
| "client/plugins" | |
| "config" | |
| "logs" | |
| "plugins" | |
| "data" | |
| ) | |
| for p in "${EXTRA_PRESERVE_PATHS[@]}"; do | |
| PRESERVE_PATHS+=("$p") | |
| done | |
| readonly BACKUP_DIR="$INSTALL_PATH/mattermost-back-$(date +'%F-%H-%M')" | |
| log "Upgrade target version: $VERSION" | |
| log "Edition: $EDITION" | |
| log "Architecture: $ARCH" | |
| log "Install path: $INSTALL_PATH" | |
| log "Mattermost directory: $MATTERMOST_DIR" | |
| log "Backup directory: $BACKUP_DIR" | |
| log "Temporary directory: $UPGRADE_DIR" | |
| log "Service: $SERVICE_NAME" | |
| log "Owner/group: $OWNER:$GROUP" | |
| log "Preserve paths: ${PRESERVE_PATHS[*]}" | |
| if [[ "$DRY_RUN" -eq 1 ]]; then | |
| log "DRY RUN mode is enabled" | |
| fi | |
| trap recover_service_on_error EXIT | |
| if [[ -z "${DOWNLOAD_URL:-}" ]]; then | |
| [[ -f "$ARCHIVE_PATH" ]] || fail "archive not found: $ARCHIVE_PATH" | |
| log "Using local archive: $ARCHIVE_PATH" | |
| else | |
| need_cmd mkdir | |
| run_cmd mkdir -p -- "$DOWNLOAD_DIR" | |
| run_cmd rm -f -- "$ARCHIVE_PATH" | |
| log "Downloading: $DOWNLOAD_URL" | |
| if command -v curl >/dev/null 2>&1; then | |
| run_cmd curl -fL "$DOWNLOAD_URL" -o "$ARCHIVE_PATH" | |
| elif command -v wget >/dev/null 2>&1; then | |
| run_cmd wget -O "$ARCHIVE_PATH" "$DOWNLOAD_URL" | |
| else | |
| fail "either curl or wget is required to download the archive" | |
| fi | |
| fi | |
| run_cmd rm -rf -- "$UPGRADE_DIR" | |
| run_cmd mkdir -p -- "$UPGRADE_DIR" | |
| log "Extracting archive to $UPGRADE_DIR" | |
| run_cmd tar -xf "$ARCHIVE_PATH" -C "$DOWNLOAD_DIR" --transform='s,^[^/]\+,mattermost-upgrade,' | |
| log "Stopping service: $SERVICE_NAME" | |
| run_root_cmd systemctl stop "$SERVICE_NAME" | |
| SERVICE_STOPPED=1 | |
| log "Creating application backup: $BACKUP_DIR" | |
| run_root_cmd mkdir -p -- "$BACKUP_DIR" | |
| run_root_cmd cp -ra "$MATTERMOST_DIR/." "$BACKUP_DIR/" | |
| cleanup_current_install "$MATTERMOST_DIR" | |
| log "Copying upgraded files to $MATTERMOST_DIR" | |
| run_root_cmd cp -an "$UPGRADE_DIR/." "$MATTERMOST_DIR/" | |
| log "Applying ownership: $OWNER:$GROUP" | |
| run_root_cmd chown -R "$OWNER:$GROUP" "$MATTERMOST_DIR" | |
| if [[ "$SETCAP_ENABLED" -eq 1 ]]; then | |
| log "Applying setcap for low port binding" | |
| run_root_cmd setcap cap_net_bind_service=+ep "$MATTERMOST_DIR/bin/mattermost" | |
| fi | |
| log "Starting service: $SERVICE_NAME" | |
| run_root_cmd systemctl start "$SERVICE_NAME" | |
| SERVICE_STOPPED=0 | |
| run_root_cmd systemctl is-active --quiet "$SERVICE_NAME" | |
| if [[ "$KEEP_TMP" -eq 0 ]]; then | |
| log "Cleaning temporary files" | |
| run_root_cmd rm -rf -- "$UPGRADE_DIR" | |
| if [[ "$ARCHIVE_WAS_DOWNLOADED" -eq 1 ]]; then | |
| run_root_cmd rm -f -- "$ARCHIVE_PATH" | |
| fi | |
| fi | |
| log "Upgrade completed successfully." | |
| trap - EXIT |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment