Posted on 03-17-2015 01:51 AM
Hi all,
Wanted to share a small script I've been using to install/update the OS X Slack app using the JSS. Found App Store app deployment considerations to be an unnecessary pain for the deployment but could not deploy a flat package with an Apple ID locked Slack app because users would not be able to update. Inspired by the recent App Store downtime
I am far from a pro developer. Happy to get any input.
Posted on 09-11-2015 07:16 AM
Thanks Owen! I've been finding it a pain as well to deploy slack in a non-app store dependent mode that is easy to update without having to get your end user to sign in every time. I'll give this a try.
Posted on 09-11-2015 09:53 AM
Great! That's exactly why I wrote it. Let me know if you run into issues with the script, I've been using it to install Slack during deployment for the last five months.
Posted on 11-20-2015 06:56 AM
Hey Owen,
This is fantastic, you've really helped me out. I am starting to have some funny issues however. Are you scripting a "quit" before and an "open" after? I'm getting some strange error messages when I do so not sure if you have experienced the same thing..
Sometimes during the quit, sometimes during the open I'll get this:
Running script Open Slack...
Script exit code: 1
Script result: LSOpenURLsWithRole() failed with error -10810 for the file /Applications/Slack.app.
Posted on 04-04-2016 01:49 PM
Thanks for this! Works great!
Posted on 04-04-2016 05:01 PM
Glad the two of you were able to use the script, I do recommend using device-based VPP to deploy Slack when possible however. The script has worked well enough, but I've seen an issue or two with automatic updates when deploying this way that I haven't taken the time to investigate yet. Re-running the script / deleting Slack from Applications and then downloading again works fine, but it's not ideal.
Let me know if issues come up.
Posted on 06-08-2016 08:29 AM
Hi Everyone,
I've been happily using this Slack installation script for months now but it suddenly stopped working in the past few weeks. When I run the script locally on a Mac, it works fine, but when I push it through Casper (either as part of my deployment workflow or from a Self-Service item), it fails with a "Cannot Install Item" error . The logs in the JSS aren't very helpful, they only read:
[STEP 1 of 4]
Executing Policy Install Slack
[STEP 2 of 4]
[STEP 3 of 4]
[STEP 4 of 4]
Is there another place to look for more detailed logs about how the script ran in the Casper context?
Thanks,
-Mike
Posted on 12-19-2017 10:30 AM
I am seeing this error now that Slack 3.0.0 is out -
/usr/bin/unzip -o -q "$slackZipPath" -d "$SLACK_UNZIP_DIRECTORY"
[/tmp/Slack-3.0.0.dmg]
End-of-central-directory signature not found. Either this file is not
a zipfile, or it constitutes one disk of a multi-part archive. In the
latter case the central directory and zipfile comment will be found on
the last disk(s) of this archive.
unzip: cannot find zipfile directory in one of /tmp/Slack-3.0.0.dmg or
/tmp/Slack-3.0.0.dmg.zip, and cannot find /tmp/Slack-3.0.0.dmg.ZIP, period.
rm -rf "$slackZipPath"
Posted on 01-08-2018 09:02 AM
I believe Slack changed their downloader from a .zip to a .dmg.
This should replace any .zip functionality you have in the script:
# Downloads the Slack Update
/usr/bin/curl --retry 3 -L "https://slack.com/ssb/download-osx" -o "/tmp/Slack.dmg"
# Mount the .dmg
hdiutil attach /tmp/Slack.dmg -nobrowse
#Print existing verion
slackVersion=$(defaults read /Applications/Slack.app/Contents/Info.plist CFBundleShortVersionString)
printf "Removing Slack version %s
" "$slackVersion"
# Remove the existing Application
rm -rf /Applications/Slack.app
# Copy the updated .app into place
cp -R /Volumes/Slack/Slack.app /Applications
# unmount the .dmg
hdiutil unmount /Volumes/Slack
Posted on 01-12-2018 01:32 PM
@mhinsz Can you expand on this? I'm a noob and can't figure out what to replace.
Posted on 01-19-2018 08:05 AM
Slack also now changed their mount dmg name to "Slack420921516146465233" so the unmount name might need to have some logic behind it as well . I will be posting a fork for this shortly.
@nimitz slack changed from a .zip download file type to a .dmg - so instead of unzipping now you need to mount the dmg and then copy the app from the Volume/Slack420921516146465233 to the Applications folder.
Posted on 01-19-2018 08:09 AM
@mhinsz Slack changed the dmg mount volume name scheme - take a look at my fork below - happy hunting!
Posted on 01-19-2018 10:00 AM
Here is my fork of @owen.pragel Slack script installer - cheers
[https://github.com/bwiessner/install_latest_slack_osx_app](link URL)
Posted on 03-21-2018 12:57 PM
Looks like your embedded link points back to this JAMFNation discussion when you click on it. Here is the link to your GitHub
https://github.com/bwiessner/install_latest_slack_osx_app
Thanks for sharing this!
Posted on 04-17-2018 02:22 PM
This script works well. Thanks for posting and sharing.
Posted on 05-09-2018 11:33 AM
Script works! Thanks for sharing!
Posted on 02-28-2019 05:03 PM
Just wanted to say this script is still working and works great!
Also just looking at it is actually quite inspirational in terms of how as a JAMF admin you can make software policies that don't have to be manually re-packaged and updated because they stay good for so long.
Only question is @owen.pragel where did you find out the site you used to query the version numbers? I couldn't figure out where you found that json link.
I hope you're still around to answer that question!
Posted on 04-30-2019 11:46 AM
Looks like Slack has changed their wrapper method from DMG to Zip. May run into issues if DMG is hard coded. Trying to work on a script which will check for what kind of file was downloaded and then either mount dmg then move or unzip to /Applications.
Posted on 04-30-2019 12:50 PM
This would be the easiest way, you don't have to care about the file extension:
#!/bin/bash
install_slack() {
download=$(curl -s "https://downloads.slack-edge.com/mac_releases/releases.json" | jq -r '.[].url' | tail -1)
echo "=> Download from '${download}'"
curl -s "${download}" -o "/tmp/$(basename ${download})"
echo "=> Install '$(basename ${download})' to '/Applications/Slack.app'"
ditto -x -k "/tmp/$(basename ${download})" "/Applications/"
rm -f "/tmp/$(basename ${download})"
}
if [ ! -s "/Applications/Slack.app" ]; then
echo "=> Slack.app is not installed"
install_slack
elif [ ! $(defaults read "/Applications/Slack.app/Contents/Info.plist" CFBundleShortVersionString) = $(curl -s "https://downloads.slack-edge.com/mac_releases/releases.json" | jq -r '.[].version' | tail -1) ]; then
echo "=> Slack.app is installed but not current"
install_slack
else
echo "=> Slack.app is installed and current"
fi
You will need to have jq
installed: https://stedolan.github.io/jq/
Posted on 06-04-2019 03:25 PM
Thanks @anverhousseini. Trying to get something going without installing the jq as a dependancy.
Posted on 06-16-2019 02:56 AM
I recreated the Slack install script without the jq dependency. Feel free to check it out Here
Posted on 07-19-2019 10:28 AM
It just came to my attention that
"https://downloads.slack-edge.com/mac_releases/releases.json will soon be removed due to a change in [Slack's] updating process.
They can hit our update endpoint, that URL looks something like:
https://slack.com/desktop/update?id=<any-random-string>&platform=darwin&os_version=<their-os-version>&app_version=<their-app-version>&channel=<their-channel>
They can find an example in their browser.log. Search for a line with the text MacUpdater: Direct feed URL and copy the URL from there."
I will be working to rewrite the Slack version pull this weekend, but the quickest fix would be to change
currentSlackVersion="4.0.0"
Posted on 07-19-2019 01:19 PM
To address Slacks removal of their releases.json link, I have updated the currentSlackVersion variable to:
Edited 8/1/19 to point to RSS link
currentSlackVersion=$(/usr/bin/curl -sL 'https://slack.com/release-notes/mac/rss' | grep -o "Slack-[0-9].[0-9].[0-9]" | cut -c 7-11 | head -n 1)
The latest version of my Slack install/update script can be found here:
https://github.com/shaquir/ShellScript/blob/master/installSlack.sh
Posted on 11-14-2019 07:30 AM
Question?
What if you currently have the latest version of Slack which at the moment is 4.1.2 if they are working on something in Slack would this script check the version and just exit if it's the latest version?
I want to add it as part of my check-in's but don't want to interrupt any of our Developers in the processes.
Posted on 04-06-2020 09:33 AM
Would this work on Catalina?
Posted on 04-06-2020 10:42 AM
I been using the following script that up to this point has been working for me not sure if it has changed for Catalina.
#!/usr/bin/env bash
################################################################################
# Custom Functions (These functions may need to be edited for specific apps)
################################################################################
extract_latest_version(){
# used to extract the latest version from a website.
# /usr/bin/grep -Eo '^.{4}$' can be used to extract a version number of a specific length.
# generic extraction code: perl -pe 'if(($_)=/([0-9]+([.][0-9]+)+)/){$_.="
"}' | /usr/bin/sort -Vu | /usr/bin/tail -n 1
perl -pe 'if(($_)=/([0-9]+([.][0-9]+)+)/){$_.="
"}' | /usr/bin/sort -Vu | /usr/bin/grep -Eo '^.{5}$' | /usr/bin/tail -n 1
}
################################################################################
# Fuctions (DO NOT EDIT THE BELOW FUNCTIONS, EXCEPT FOR MAIN)
################################################################################
message(){
# description of what function does.
local description='Displays a message to the customer if the defined application is running. Allow the customer to cancel if needed.'
# define local variables.
local message="${1}"
local title='IT Support'
local jamfHelper='/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
local cancelMessage='User chose to cancel the update the process.'
# data validation.
[[ -z "${applicationName}" ]] && applicationName='Application'
[[ -z "${applicationState}" ]] && applicationState=0
[[ -z "${message}" ]] && message='Press OK to continue'
# display dialog box message if application is running, otherwise continue silently.
if [[ -e "${jamfHelper}" && "${applicationState}" -eq 1 ]]; then
if ! "${jamfHelper}"
-windowType hud
-title "${title}"
-heading "${applicationName} Update"
-button1 'OK'
-button2 'Cancel'
-description "${message}"
-defaultButton 1
-lockHUD &>/dev/null
then
printf '%s
' "ERROR: ${cancelMessage}" 1>&2
exit 1
fi
fi
}
error(){
# description of what function does.
local description='Displays an error message to the customer if the defined application is running. Otherwise prints to STDERR.'
# declare local variables.
local errorMessage="${1}"
local title='Block.one IT Support'
local jamfHelper='/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
local defaultMessage='Update Failed.'
# data validation
[[ -z "${applicationName}" ]] && applicationName='Application'
[[ -z "${applicationState}" ]] && applicationState=0
[[ -z "${errorMessage}" ]] && errorMessage='something went wrong. update failed.'
# display error message to customer only if application is running, otherwise print to STDERR.
if [[ -e "${jamfHelper}" && "${applicationState}" -eq 1 ]]; then
"${jamfHelper}"
-windowType hud
-title "${title}"
-heading "${applicationName} Update"
-button1 'OK'
-description "${defaultMessage}"
-defaultButton 1
-lockHUD &>/dev/null &
printf '%s
' "ERROR: ${errorMessage}" 1>&2
exit 1
else
printf '%s
' "ERROR: ${errorMessage}" 1>&2
exit 1
fi
}
get_installed_version(){
# description of what function does.
local description='Read and return version information from the Info.plist of the defined application.(aka: obtain version information for currently installed application.)'
# define local variables.
local applicationPath="${1}"
local installedVersion
# if the application path is defined and is a directory attempt to read and return version information
[[ -z "${applicationPath}" || ! -d "${applicationPath}" ]] && error 'application not installed or path undefined.'
installedVersion="$( /usr/bin/defaults read "${applicationPath}"/Contents/Info CFBundleShortVersionString 2> /dev/null )" || error 'could not detect installed version.'
[[ -z "${installedVersion}" ]] && error 'installed version undefined.'
# return installed version.
printf '%s
' "${installedVersion}"
}
get_latest_version(){
# description of what function does.
local description='Extracts latest version information from a URL.'
# define local variables.
local latestVersionUrl="${1}"
local data
local latestVersion
# attempt to extract version information from URL and return value.
[[ -z "${latestVersionUrl}" ]] && error 'URL to search for latest version undefined.'
data="$( /usr/bin/curl -sLJ "${latestVersionUrl}" )" || error 'failed to download version data.'
[[ -z "${data}" ]] && error 'failed to download version data.'
latestVersion="$( printf '%s
' "${data}" | extract_latest_version )" || error 'failed to extract latest version.'
[[ -z "${latestVersion}" ]] && error 'latest version undefined.'
# return latest version.
printf '%s
' "${latestVersion}"
}
compare_versions(){
# description of what function does.
local description='Determines if the installed and latest versions are equal or not.'
# define local variables.
local latestVersion="${1}"
local installedVersion="${2}"
# data validation.
[[ -z "${latestVersion}" ]] && error 'latest version undefined.'
[[ -z "${installedVersion}" ]] && error 'installed version undefined.'
# use the sort commands built-in ability to sort version numbers.
if [[ "$( printf '%s
' "${latestVersion}" "${installedVersion}" | /usr/bin/sort -V | /usr/bin/head -n 1 )" != "${latestVersion}" ]]; then
printf '%s
' 'application needs to be updated.'
elif [[ "${latestVersion}" != "${installedVersion}" ]]; then
error 'installed version is newer. latest version URL may need to be updated.'
else
printf '%s
' 'application is on the latest version.'
exit 0
fi
}
download(){
# description of what function does.
local description='Downloads a file from a given URL to a temporary directory and returns the full path to the download.'
# define local variables.
local dlURL="${1}"
dlDir=''
local dlName
local productVer
local userAgent
downloadPath=''
# if the download URL was provided. Build the effective URL (this helps if the given URL redirects to a specific download URL.)
[[ -z "${dlURL}" ]] && error 'download url undefined.'
dlURL="$( /usr/bin/curl "${dlURL}" -s -L -I -o /dev/null -w '%{url_effective}' )" || error 'failed to determine effective URL.'
# create temporary directory for the download.
dlDir="$( /usr/bin/mktemp -d 2> /dev/null )" || error 'failed to create temporary download directory.'
[[ ! -d "${dlDir}" ]] && error 'temporary download directory does not exist.'
export dlDir
# build user agent for curl.
productVer="$( /usr/bin/sw_vers -productVersion | /usr/bin/tr '.' '_' )" || error 'could not detect product version needed for user agent.'
[[ -z "${productVer}" ]] && error 'product version undefined'
userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X '"${productVer})"' AppleWebKit/535.6.2 (KHTML, like Gecko) Version/5.2 Safari/535.6.2'
# change the present working directory to the temporary download directory and attempt download.
cd "${dlDir}" || error 'could not change pwd to temporary download directory.'
dlName="$( /usr/bin/curl -sLJO -A "${userAgent}" -w "%{filename_effective}" --retry 10 "${dlURL}" )" || error 'failed to download latest version.'
[[ -z "${dlName}" ]] && error 'download filename undefined.'
downloadPath="${dlDir}/${dlName}"
[[ ! -e "${downloadPath}" ]] && error 'download filename undefined. can not locate download.'
# export full path to the downloaded file including extension.
export downloadPath
}
detect_running(){
# description of what function does.
local description='Detect if the defined application is currently running. Export the application state so it is available globally.'
# define variables.
applicationState=0
# determine if application is running and return result.
# 1 = running , 0 = not running.
if /usr/bin/pgrep -q "${applicationName}"; then
applicationState=1
export applicationState
else
export applicationState
fi
}
kill_running(){
# description of what function does.
local description='If the defined application is running. Give customer option to close it or cancel the update process.'
# notify customer and get input. attempt killing application if it is running and customer has agreed.
message "${applicationName} needs to be updated. The application will close if you continue."
if [[ "${applicationState}" -eq 1 ]]; then
/usr/bin/pkill -9 "${applicationName}" &>/dev/null
fi
}
uninstall(){
# description of what function does.
local description='Uninstalls the defined application.'
# define local variables.
local applicationPath="${1}"
# data validation.
[[ ! -d "${applicationPath}" ]] && error 'app path undefined or not a directory.'
# attempt uninstall.
/bin/mv "${applicationPath}" "${applicationPath}.old" &> /dev/null || error "failed to uninstall application."
sleep 2
}
install(){
# description of what function does.
local description='Determines what kind of installer the download is. Attempts install accordingly.'
# determine download installer type. (dmg, pkg, zip)
if [[ "$( printf '%s
' "${downloadPath}" | /usr/bin/grep -c '.dmg$' )" -eq 1 ]]; then
install_dmg
elif [[ "$( printf '%s
' "${downloadPath}" | /usr/bin/grep -c '.pkg$' )" -eq 1 ]]; then
install_pkg
elif [[ "$( printf '%s
' "${downloadPath}" | /usr/bin/grep -c '.zip$' )" -eq 1 ]]; then
install_zip
else
error 'could not detect install type.'
fi
}
install_pkg(){
# description of what function does.
local description='Silently install pkg.'
# define local variables.
local pkg="${1}"
if [[ -z "${pkg}" ]]; then
pkg="${downloadPath}"
fi
# use installer command line tool to silently install pkg.
/usr/sbin/installer -allowUntrusted -pkg "${pkg}" -target / &> /dev/null || error 'failed to install latest version pkg.'
}
install_dmg(){
# description of what function does.
local description='Silently install dmg.'
# define variables.
mnt=''
local dmg="${1}"
local app
local pkg
if [[ -z "${dmg}" ]]; then
dmg="${downloadPath}"
fi
# create temporary mount directory for dmg and export path if exists.
mnt="$( /usr/bin/mktemp -d 2> /dev/null )" || error 'failed to create temporary mount point for dmg.'
[[ ! -d "${mnt}" ]] && error 'failed to verify temporary mount point for dmg exists.'
export mnt
# silently attach the dmg download to the temporary mount directory and determine what it contains (app or pkg)
sleep 2
/usr/bin/hdiutil attach "${dmg}" -quiet -nobrowse -mountpoint ${mnt} &> /dev/null || error 'failed to mount dmg.'
app="$( /bin/ls "${mnt}" | /usr/bin/grep '.app$' | head -n 1 )"
pkg="$( /bin/ls "${mnt}" | /usr/bin/grep '.pkg$' | head -n 1 )"
# attempt install based on contents of dmg.
if [[ ! -z "${app}" && -e "${mnt}/${app}" ]]; then
cp -Rf "${mnt}/${app}" '/Applications' &> /dev/null || error "failed to copy the latest version to the applications directory."
elif [[ ! -z "${pkg}" && -e "${mnt}/${pkg}" ]]; then
install_pkg "${mnt}/${pkg}"
else
error 'could not detect installation type in mounted dmg.'
fi
}
install_zip(){
# define variables.
uz=''
local app
local pkg
local dmg
# create temporary unzip directory and export globally if exists.
uz="$( /usr/bin/mktemp -d 2> /dev/null )" || error 'failed to create temporary unzip directory for zip.'
[[ ! -d "${uz}" ]] && error 'failed to verify temporary unzip directory exists.'
export uz
# unzip zip file and determine installer type. (app, pkg, dmg)
/usr/bin/unzip "${downloadPath}" -d "${uz}" &> /dev/null || error 'failed to unzip download.'
app="$( /bin/ls ${uz} | /usr/bin/grep '.app$' | head -n 1 )"
pkg="$( /bin/ls ${uz} | /usr/bin/grep '.pkg$' | head -n 1 )"
dmg="$( /bin/ls ${uz} | /usr/bin/grep '.dmg$' | head -n 1 )"
# attempt install based on contents of zip file.
if [[ ! -z "${app}" && -e "${uz}/${app}" ]]; then
cp -Rf "${uz}/${app}" '/Applications' &> /dev/null || error "failed to copy the latest version to the applications directory."
elif [[ ! -z "${pkg}" && -e "${uz}/${pkg}" ]]; then
install_pkg "${uz}/${pkg}"
elif [[ ! -z "${dmg}" && -e "${uz}/${dmg}" ]]; then
install_dmg "${uz}/${dmg}"
else
error 'could not detect installation type in unzipped download.'
fi
}
cleanup(){
# description of what function does.
local description='Removes temporary items created during the download and installation processes.'
local applicationPath="/Applications/${applicationName}.app"
# if a temporary mount directory has been created, force unmount and remove the directory.
if [[ -d "${mnt}" ]]; then
/usr/bin/hdiutil detach -force -quiet "${mnt}"
/sbin/umount -f "${mnt}" &> /dev/null
/bin/rm -rf "${mnt}" &> /dev/null
fi
# if temporary unzip directory exists, remove it.
if [[ -d "${uz}" ]]; then
/bin/rm -rf "${uz}" &> /dev/null
fi
# if the defined application does not exist restore the original to the apps directory.
if [[ ! -d "${applicationPath}" ]]; then
printf '%s
' 'Update failed. Restoring original application...'
/bin/mv "${applicationPath}.old" "${applicationPath}" &> /dev/null
elif [[ -d "${applicationPath}.old" ]]; then
/bin/rm -rf "${applicationPath}.old" &> /dev/null
fi
# if a temporary download directory has been created. remove it.
if [[ -d "${dlDir}" ]]; then
/bin/rm -rf "${dlDir}" &> /dev/null
fi
}
main(){
# declare local variables
applicationName='Slack'
local latestVersionUrl='https://slack.com/downloads/mac'
local latestDownloadUrl='https://slack.com/ssb/download-osx'
local applicationPath="/Applications/${applicationName}.app"
local installedVersion
local latestVersion
# ensure cleanup runs on exit or error.
trap cleanup EXIT ERR
# export global variables
export applicationName
# determine if the application needs to be updated.
installedVersion="$( get_installed_version "${applicationPath}" )" || exit 1
latestVersion="$( get_latest_version "${latestVersionUrl}" )" || exit 1
compare_versions "${latestVersion}" "${installedVersion}"
# download latest version of the application and export full path to the temporary download location for the cleanup function.
download "${latestDownloadUrl}"
# determine if application is running and notify customer before attempting kill.
detect_running
kill_running
# uninstall the application if neeeded for the update.
uninstall "${applicationPath}"
# install latest version of the application.
install
message 'Update Successful'
exit 0
}
main "$@"
Posted on 04-07-2020 08:30 AM
Thanks @JarvisUno I will test it out soon.
Posted on 02-11-2021 05:13 AM
Anybody has a script to install and download the latest version of Slack?
Posted on 03-08-2021 12:30 AM
Any script for M1 chip laptop
Posted on 03-19-2021 03:44 PM
This script works for me. It is based on the Google Chrome DL and Install script found here: https://www.jamf.com/jamf-nation/discussions/20894/install-latest-version-of-google-chrome-without-r...
#!/bin/sh
dmgfile="Slack.dmg"
volname="Slack.app"
logfile="/Library/Logs/SlackInstallScript.log"
url='https://slack.com/ssb/download-osx'
/bin/echo "--" >> ${logfile}
/bin/echo "`date`: Downloading latest version." >> ${logfile}
/usr/bin/curl -L -o /tmp/${dmgfile} "${url}" >/dev/null 2>&1
/bin/echo "`date`: Mounting installer disk image." >> ${logfile}
/usr/bin/hdiutil attach /tmp/${dmgfile} -nobrowse -quiet
/bin/echo "`date`: Installing..." >> ${logfile}
ditto -rsrc "/Volumes/${volname}/Slack.app" "/Applications/Slack.app"
/bin/sleep 10
/bin/echo "`date`: Unmounting installer disk image." >> ${logfile}
/usr/bin/hdiutil detach $(/bin/df | /usr/bin/grep "${volname}" | awk '{print $1}') -quiet
/bin/sleep 10
/bin/echo "`date`: Deleting disk image." >> ${logfile}
/bin/rm /tmp/"${dmgfile}"
exit 0
Posted on 05-21-2021 09:42 AM
Hey All,
Thought I would post this to help others. I found the links for the 3 different installers:
Intel = curl "https://slack.com/ssb/download-osx" -s -L -I -o /dev/null -w '%{url_effective}'
Silicon = curl "https://slack.com/ssb/download-osx-silicon" -s -L -I -o /dev/null -w '%{url_effective}'
Universal = curl "https://slack.com/ssb/download-osx-universal" -s -L -I -o /dev/null -w '%{url_effective}'
Article pulled from: https://slack.com/help/articles/360035635174-Deploy-Slack-for-macOS
Hope this helps
Shawn OG