Skip to content

Instantly share code, notes, and snippets.

@Roy-Orbison
Last active December 18, 2025 06:59
Show Gist options
  • Select an option

  • Save Roy-Orbison/e7a24ad07475d8af140b6f8900efbb62 to your computer and use it in GitHub Desktop.

Select an option

Save Roy-Orbison/e7a24ad07475d8af140b6f8900efbb62 to your computer and use it in GitHub Desktop.
Shell script to check for latest version of Mattermost Server, including patch releases, optionally filtered to ESR. Alternate, improved Python version as well.

Check for Mattermost updates

Mattermost still won't alert admins properly to security patch releases, and does not care whether you prefer the stability of ESR versions. It tells admins that fully patched and supported systems are a Problem, in bold red type on the Workspace Optimisation page of the System Console.

Here are a couple of scripts to run with cron, which actively alert you to updates. The shell script is simpler and assumes you want to be on the very latest version, or latest ESR version. The Python version lists all available updates and whether your major + minor version is currently supported.

Call one with env vars like so:

MM_HOOK=https://your-mm-server.example/hooks/abcdefghijklmnopqrstuvwxyz \
	MM_CHANNEL=@your_username \
	MM_ESR=1 \
	mattermost-version-check

Its advisable to put those values in a config file then source it, e.g.:

(set -a && . ./.mm-vc-env && mattermost-version-check.py)

You can override the system user account and path to binary with MM_USER and MM_BIN, if you do not use the defaults.

#!/bin/sh
set -e
{
read -r version_current
read -r ed
} <<-EOSH
$(sudo -u "${MM_USER:-mattermost}" "${MM_BIN:-/opt/mattermost/bin/mattermost}" version | \
grep -Pio '^(?:version|build enterprise ready):\s*\K\S+')
EOSH
if [ "$ed" != true ]; then
ed=Team
else
ed=Enterprise
fi
if [ "${MM_ESR:-0}" -gt 0 ]; then
esr=1
else
esr=0
fi
index=https://raw.githubusercontent.com/mattermost/docs/refs/heads/master/source/product-overview/version-archive.rst
index=$(curl --no-progress-meter "$index")
[ "$index" ]
latest=$(
awk -v ed=$ed -v esr=$esr "$(cat <<-'EOAWK'
BEGIN {
preamble = 1
RS = "\n +Mattermost " ed " Edition v"
FS = "\n +-"
}
preamble {
preamble=0
next
}
{
if (esr && !match($1, /\(ESR\)/))
next
v = gensub(/[[:space:]].*/, "", 1, $1)
if (!match($3, "SHA-256")) {
print "no checksum found"
exit 1
}
u = gensub(/^[^`]*`+|`.*/, "", "g", $2)
c = gensub(/^[^`]*`+|`.*/, "", "g", $3)
printf "%s\t%s\t%s\n", v, u, c
exit
}
EOAWK
)" <<-EORST
$index
EORST
)
read -r version_latest archive checksum <<-EOT
$latest
EOT
if [ "$version_current" != "$version_latest" ]; then
curl -sS --json @- "$MM_HOOK" <<-EOJSON > /dev/null
{
"channel": "$MM_CHANNEL",
"text": ":information_source: Mattermost upgrade available: [v$version_latest]($archive)\\nChecksum: \`$checksum\`\\n"
}
EOJSON
fi
#!/usr/bin/env python3
import os
import sys
import subprocess
import requests
import json
import re
import datetime
ua = 'Update Checker for Mattermost'
url_base = 'https://raw.githubusercontent.com/mattermost/docs/refs/heads/master/source/product-overview/'
url_support = url_base + 'mattermost-server-releases.md'
url_index = url_base + 'version-archive.rst'
notify_channel = os.getenv('MM_CHANNEL', '')
notify_url = os.getenv('MM_HOOK', '')
esr = bool(re.match(r'(?:1|t|y)', os.getenv('MM_ESR', '')))
user = os.getenv('MM_USER', 'mattermost')
mm = os.getenv('MM_BIN', '/opt/mattermost/bin/mattermost')
sys.stderr.write('Checking for new version…')
def vuple(version: str):
return tuple(map(int, version.split('.')))
def get_current():
r = subprocess.run(
['sudo', '-u', user, mm, 'version'],
capture_output=True,
text=True,
check=True,
)
values = {}
for line in r.stdout.splitlines():
kv = re.split(r':\s+', line, maxsplit=2)
if len(kv) == 2:
values[kv[0]] = kv[1]
if not values:
raise Exception('No version values found.')
return values
def rload(url: str):
r = requests.get(
url,
cookies=rload.cookies,
headers={
'User-Agent': ua,
},
timeout=7,
)
if r.status_code != 200:
raise Exception('Remote file fetch failed.')
rload.cookies = r.cookies
return r.text
rload.cookies = {}
def get_support():
text = re.search(r'^#+\s*Latest\b.*', rload(url_support), re.M | re.S)
if not text:
raise Exception('Remote support file changed format.')
values = {}
for line in text[0].splitlines():
version = re.match(r'\s*\|\s*v(?P<v>\d+\.\d+)\b.+?\|\s*(?P<date>\d+(?:-\d+){2})\b[^|]*\|\s*$', line)
if version:
values[version['v']] = version['date']
if not values:
raise Exception('No support dates found.')
return values
def get_index(ent: bool, current_minor: str, current_minor_tup: tuple):
if ent:
pat_ed = 'Enterprise'
else:
pat_ed = 'Team'
pat_ed = r'^[ \t]+Mattermost ' + pat_ed + r' Edition v'
text = re.search(pat_ed + '.*', rload(url_index), re.M | re.S)
except_format = 'Remote archive index file changed format.'
if not text:
raise Exception(except_format)
values = {}
value = None
for line in text[0].splitlines():
version = re.match(pat_ed + r'(?P<v>\d+(?:\.\d+){2})\s+(?:.+?(?P<esr>\bESR\b))?.+?`Download\s+<(?P<archive_download>.+?)>`', line)
if version:
if (
value and
(
not esr or
value['esr'] or
current_minor == value['minor']
)
):
values[value['minor']] = value
value = version.groupdict()
value['minor'] = re.match(r'\d+\.\d+', value['v'])[0]
if current_minor_tup > vuple(value['minor']):
value = None
break
value['esr'] = bool(value['esr'])
elif not value:
raise Exception(except_format)
else:
detail = re.match(r'\s+-\s*(?:(?P<label>\S+(?:\s+\S+)*):\s+)?((?:``)?)(?P<value>.+)\2$', line)
if not detail:
raise Exception(except_format)
detail = detail.groupdict('archive')
value[detail['label']] = detail['value']
if (
value and
(
not esr or
value['esr'] or
current_minor == value['minor']
)
):
values[value['minor']] = value
if not values:
raise Exception('No versions found.')
return values
current = get_current()
if current.get('Version') is None:
raise Exception('Unexpected output from local version check.')
current_minor = re.match(r'\d+\.\d+', current['Version'])
if not current_minor:
raise Exception('Unexpected format from local version check.')
current_minor = current_minor[0]
current_minor_tup = vuple(current_minor)
messages = []
support = get_support()
message_eol = ':warning: Your Mattermost version has reached end of life. Upgrade to a supported version.'
if support.get(current_minor) is None:
if current_minor_tup < vuple(list(support.keys())[-1]):
messages.append(message_eol)
else:
messages.append(':interrobang: Support for your Mattermost version is indeterminate.')
elif support.get(current_minor) < datetime.datetime.now().strftime('%F'):
messages.append(message_eol)
index = get_index(
current.get('Build Enterprise Ready', 'true') != 'false',
current_minor,
current_minor_tup,
)
details_minor = index.get(current_minor)
if not details_minor:
messages.append(':interrobang: Your Mattermost version was not found in the archive index.')
elif (current_tup := vuple(current['Version'])) >= (patch_tup := vuple(details_minor['v'])):
if current_tup != patch_tup:
messages.append(':interrobang: Your Mattermost version is newer than expected.')
details_minor = None
del index[current_minor]
if index:
if not notify_url:
sys.stderr.write(' done.\nUpgrades available: v%s\n' % (
', v'.join([index[minor]['v'] for minor in index]),
))
sys.exit(1)
messages.extend([
':information_source: Mattermost upgrade%s available:' % (
's' if len(index) > 1 else '',
),
'',
'|Version|ESR|Checksum|',
'|---:|---|---|',
])
for minor in index:
details = index[minor]
messages.append('|[v%s](%s)|%s|`%s`|' % (
details['v'],
details['archive_download'],
':white_check_mark:' if details['esr'] else '',
details.get('SHA-256 Checksum', '???'),
))
body = {
'text': '\n'.join(messages),
}
if notify_channel:
body['channel'] = notify_channel
r = requests.post(notify_url, json=body)
r.raise_for_status()
sys.stderr.write(' done.\n')
else:
sys.stderr.write(' none.\n')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment