Skip to content

Instantly share code, notes, and snippets.

@miklosboros
Last active February 26, 2025 11:46
Show Gist options
  • Save miklosboros/4855a59213724f6eb5960579e15d285b to your computer and use it in GitHub Desktop.
Save miklosboros/4855a59213724f6eb5960579e15d285b to your computer and use it in GitHub Desktop.
#!/bin/bash
#
# Uploads an installable artifact to Bitrise using Release Management Public API.
# Reference: https://api.bitrise.io/release-management/api-docs/index.html#/Installable%20Artifacts%20-%20COMING%20SOON/GenerateInstallableArtifactUploadUrl
#
# This script supports Linux distributions (alpine, arch, centos, debian, fedora, rhel, ubuntu) and macOS.
# For it to work properly you will need either jq and openssl packages installed on your system or sudo privileges for the script.
#
# You need a couple of environment variables to set up and you can call this script from terminal:
# ARTIFACT_PATH=LOCAL_PATH_OF_THE_ARTIFACT_TO_BE_UPLOADED \
# AUTHORIZATION_TOKEN=BITRISE_RM_API_ACCESS_TOKEN \
# CONNECTED_APP_ID=APP_ID_OF_THE_CONNECTED_APP_THE_ARTIFACT_WILL_BE_UPLOADED_TO \
# /bin/bash ./scripts/upload_installable_artifact.sh
#######################################
# Checks for script dependencies. If the OS is not supported it exits. Missing dependencies (jq, openssl) are installed.
# Globals:
# None
# Arguments:
# None
#######################################
check_dependencies() {
if [[ $(is_macOS) -eq 1 ]] && [[ $(linux_distro) -eq 1 ]]; then
echo "Unsupported OS. Exiting..."
exit 1
fi
if [[ $(check_command_installed "jq") -eq 1 ]]; then
install_command "jq"
fi
if [[ $(check_command_installed "openssl") -eq 1 ]]; then
install_command "openssl"
fi
}
#######################################
# Checks Whether the given command is installed or not.
# Globals:
# None
# Arguments:
# The command to check for.
# Outputs:
# Returns zero if the command is installed and 1 if not.
#######################################
check_command_installed() {
if command -v "$1" > /dev/null 2>&1; then
echo 0
else
echo 1
fi
}
#######################################
# Gets the information needed for uploading an installable artifact from Release Management Public API.
# Globals:
# AUTHORIZATION_TOKEN
# ARTIFACT_PATH
# CONNECTED_APP_ID
# Arguments:
# UUID for the artifact to be uploaded.
# Outputs:
# Returns the upload information including headers, method and url.
#######################################
get_upload_information() {
if [[ $(linux_distro) -ne 1 ]]; then
file_size_bytes=$(stat -c%s "$ARTIFACT_PATH")
else
file_size_bytes=$(stat -f%z "$ARTIFACT_PATH")
fi
file_name=$(echo "\"$ARTIFACT_PATH\"" | jq -r 'split("/") | .[-1]')
upload_info=$(curl -s -H "Authorization: $AUTHORIZATION_TOKEN" "$RM_API_HOST/release-management/v1/connected-apps/$CONNECTED_APP_ID/installable-artifacts/$1/upload-url?file_name=$file_name&file_size_bytes=$file_size_bytes")
echo "$upload_info"
}
#######################################
# Installs a missing command to the machine.
# Globals:
# None
# Arguments:
# The command to be installed.
#######################################
install_command() {
if [[ $(is_macOS) -eq 0 ]]; then
if command -v brew > /dev/null 2>&1; then
brew install "$1"
else
echo "Homebrew is not installed. Please install Homebrew first."
exit 1
fi
else
distro=$(linux_distro)
printf "Detected Linux distribution: %s.\n" "$(distro)"
case "$distro" in
ubuntu|debian)
sudo apt update && sudo apt install -y "$1"
;;
fedora)
sudo dnf install -y "$1"
;;
centos|rhel)
sudo yum install -y "$1"
;;
alpine)
sudo apk add --no-cache "$1"
;;
arch)
sudo pacman -Sy "$1"
;;
esac
fi
}
#######################################
# Checks whether the script is running on MacOS or not.
# Globals:
# None
# Arguments:
# None
# Outputs:
# Returns zero if MacOS found, 1 otherwise.
#######################################
is_macOS() {
if [ "$(uname)" == "Darwin" ]; then
echo 0
else
echo 1
fi
}
#######################################
# Continuously checks whether the already uploaded artifact is processed by Release Management or not.
# After successful processing, you can use the uploaded artifact in your releases and test distributions.
# The function returns with a failure after a pre-defined retry count.
# This is a recursive function calling itself four times after the first try.
# Globals:
# AUTHORIZATION_TOKEN
# CONNECTED_APP_ID
# Arguments:
# UUID for the artifact to be uploaded.
# Retry count.
#######################################
is_processed() {
if [[ $2 == 4 ]]; then
echo "The artifact is still not processed after $2 retries. Exiting..."
exit 1
fi
status_data=$(curl -s -H "Authorization: $AUTHORIZATION_TOKEN" "$RM_API_HOST/release-management/v1/connected-apps/$CONNECTED_APP_ID/installable-artifacts/$1/status")
request_error "$status_data"
status=$(echo "$status_data" | jq -r '.status')
if [[ "$status" == "processed_valid" ]] || [[ "$status" == "processed_invalid" ]]; then
echo "$status_data"
exit 0
elif [[ "$status" == "uploaded" ]] || [[ "$status" == "upload_requested" ]]; then
echo "$status_data"
sleep 1
is_processed "$1" $2 + 1
else
echo "Unexpected status: $status. Exiting..."
exit 1
fi
}
#######################################
# Checks the Linux distribution the script is running on.
# Globals:
# None
# Arguments:
# None
# Outputs:
# Returns the id of the OS distribution and zero if a supported distribution is found. Returns 1 if not a Linux.
#######################################
linux_distro() {
if [ -f /etc/os-release ]; then
id=$(grep -E '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
case "$id" in ubuntu|debian|fedora|centos|rhel|alpine|arch)
echo "$id"
;;
*)
echo "Unsupported Linux distribution. Exiting..."
exit 1
;;
esac
else
echo 1
fi
}
#######################################
# Processes the response of Google Cloud Storage when the upload request has been sent.
# Globals:
# None
# Arguments:
# The upload response.
# The artifact UUID used for uploading.
# Outputs:
# Returns upload http status and response body from the upload request.
process_upload_response() {
http_status_code="${1:${#1}-3}"
if [[ "$http_status_code" == 200 ]]; then
is_processed "$2" 0
else
response_body="${1:0:${#1}-3}"
printf "upload http status: %s\n" "$http_status_code"
echo "$response_body"
fi
}
#######################################
# Checks for a request error coming from Release Management Public API.
# Globals:
# None
# Arguments:
# The response body.
# Outputs:
# Returns the error if there is an error.
request_error() {
error_code=$(echo "$1" | jq '.code' )
if [ "$error_code" == "null" ]; then
return
fi
echo "$1"
exit 0
}
#######################################
# Uploads the installable artifact to Google Cloud Storage using the information given by Release Management Public API.
# Globals:
# The artifact path which contains the file to be uploaded.
# Arguments:
# The upload information given by Release Management Public API.
# Outputs:
# Returns the response of Google Cloud Storage.
upload_artifact() {
headers_json=$(echo "$1" | jq -r '.headers | to_entries | map("\(.value.name): \(.value.value)")')
method=$(echo "$1" | jq -r '.method')
url=$(echo "$1" | jq -r '.url')
# read headers into bash array from jq array
headers=()
while IFS= read -r line; do
headers+=($line)
done <<< "$headers_json"
# sanitize headers
for ((i = 0; i < ${#headers[@]}; i++)); do
headers[i]="${headers[i]//\"/}"
headers[i]="${headers[i]%,}"
done
# build curl command
curl_command="curl -sw \"%{http_code}\" -o - -X \"$method\""
for ((i = 1; i + 1 < ${#headers[@]}; i+=2)); do
curl_command+=" -H \"${headers[i]} ${headers[i+1]}\""
done
curl_command+=" --upload-file \"$ARTIFACT_PATH\" \"$url\""
eval "$curl_command"
}
check_dependencies
uuid=$(openssl rand -hex 16)
installable_artifact_id=${uuid:0:8}-${uuid:8:4}-${uuid:12:4}-${uuid:16:4}-${uuid:20:12}
if [ -z "$RM_API_HOST" ]; then
RM_API_HOST="https://api.bitrise.io"
fi
upload_info=$(get_upload_information "$installable_artifact_id")
request_error "$upload_info"
upload_response=$(upload_artifact "$upload_info")
process_upload_response "$upload_response" "$installable_artifact_id"
@netural-philipp-gabriel

@miklosboros
I think there might be a problem with this line: https://gist.github.com/miklosboros/4855a59213724f6eb5960579e15d285b#file-bitrise_upload_installable_artifact-sh-L66.
In the comments it says: Returns 1 if not a Linux., however stat -c%s is the correct command for Linux distros. It should be the inverse logic.

@miklosboros
Copy link
Author

@miklosboros I think there might be a problem with this line: https://gist.github.com/miklosboros/4855a59213724f6eb5960579e15d285b#file-bitrise_upload_installable_artifact-sh-L66. In the comments it says: Returns 1 if not a Linux., however stat -c%s is the correct command for Linux distros. It should be the inverse logic.

I think you are right and it works properly only because the linux_distro() gives back bad response.

@netural-philipp-gabriel

I only found this, because I ran this script in an alpine docker image and it told me, that the stat properties are wrong. So I think, currently this script does not work with Linux distros.

@miklosboros
Copy link
Author

You were right. I have fixed a couple of problems for Linux distros, but I suggest to use the more "official" script maintained within our company for this: Upload script. This has only been created recently.

However, you might have problems within a docker image as 'sudo' is not always available there to install commands (if you already have jq and openssl installed within the container it won't matter tho.

I will delete this gist in a couple of weeks to not have to maintain this in two places.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment