#!/usr/bin/env bash
# https://addons-server.readthedocs.io/en/latest/topics/api/v4_frozen/signing.html#v4-upload-version
#
# consts
#
BASE_URL="https://addons.mozilla.org"
AMO_API_URL="$BASE_URL/api/v4/addons"
#
# functions
#
# Array to hold the names of created temporary files and directories
temp_items=()
# Function to create a temporary file or directory
create_temp() {
local temp_item
local is_directory=false
# Check for the directory switch
if [[ "$1" == "--dir" ]]; then
is_directory=true
shift # Remove the switch from the arguments
fi
if $is_directory; then
temp_item=$(mktemp -d) # Create a temporary directory
else
temp_item=$(mktemp) # Create a temporary file
fi
if [[ $? -eq 0 ]]; then
temp_items+=("$temp_item") # Add the temp item to the array
echo "$temp_item" # Return the name of the temp item
else
echo "Failed to create temporary item" >&2
return 1
fi
}
# Function to clean up all created temporary files and directories
cleanup_temp_items() {
for item in "${temp_items[@]}"; do
if [[ -e "$item" ]]; then
rm -rf "$item" # Remove the temporary item
echo "Removed temporary item: $item"
fi
done
temp_items=() # Clear the array after cleanup
}
# Trap to ensure cleanup on exit
trap cleanup_temp_items EXIT
# Function to calc the next version to upload to AMO
get_next_version() {
local CURRENT_AMO_VERSION="$1"
local CURRENT_AMO_VERSION_PARTS=($(echo -n "$CURRENT_AMO_VERSION" | tr '.' ' ' ))
local LAST_PART="${CURRENT_AMO_VERSION_PARTS[2]}"
local NEW_LAST_PART=$((LAST_PART+1))
echo "${CURRENT_AMO_VERSION_PARTS[0]}.${CURRENT_AMO_VERSION_PARTS[1]}.${NEW_LAST_PART}"
}
# Function to download the content of a remote
get_remote_content() {
local url="$1"
local outfile=$(create_temp);
local status=$(curl -s -L --url "$url" -w "%{http_code}" -o $outfile)
if [ $status -ne 200 ];then
echo ""
cat $outfile
echo ""
echo "ERROR: failed to download $url got status: $status"
exit 1
fi
cat $outfile
}
# Function to base64url encode ... without padding ===
b64url_encode() {
cat /dev/stdin | base64 -w0 | tr -d '=' | tr '/+' '_-'
}
# function to generate the jwt auth token
generate_auth_token() {
local header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | b64url_encode)
local iat=$(date +%s)
local jti=$((iat+RANDOM))
local exp=$((iat+30))
local payload=$(echo -n "{\"iss\":\"${ISSUER}\",\"iat\":${iat},\"jti\":\"${jti}\",\"exp\":$exp}" | b64url_encode)
local header_payload=$(echo -n "${header}.${payload}")
local signature=$(echo -n "${header_payload}" | openssl dgst -binary -sha256 -hmac "${SECRET}" | b64url_encode)
echo "${header_payload}.${signature}"
}
# function to upload the generated xpi
upload_xpi(){
local guid="$1"
local version="$2"
local xpi="$3"
local outfile=$(create_temp)
# note -g --globoff allows curly braces which are part of the GUID in the URL
local status=$(curl -s -L --url "$AMO_API_URL/$guid/versions/$version/" \
-g \
-X PUT \
--form "upload=@$xpi" \
-H "Authorization: JWT $(generate_auth_token)" \
-w "%{http_code}" \
-o $outfile
)
if [ $status -ne 202 ];then
echo ""
cat $outfile
echo ""
echo "ERROR: addon not accepted with status: $status"
exit 1
fi
}
check_deps(){
MISSING=0
for req in "$@";do
type $req >/dev/null 2>&1 || { echo >&2 "ERROR: missing dependency: $req"; MISSING=1; }
done
[ $MISSING -ne 0 ] && { echo "ERROR: aborting due to missing dependencies"; exit 1; }
}
check_vars(){
MISSING=0
for req in "$@";do
[ -z "${!req}" ] && { echo "ERROR: Missing variable: $req" >&2; MISSING=1; }
done
[ $MISSING -ne 0 ] && { echo "ERROR: aborting due to missing variables"; exit 1; }
}
update_version() {
local fp="$1"
local ver="$2"
if [ ! -f $fp ];then
echo "ERROR: no manifest.json"
exit 1
fi
# update manifest.json with new version
local tmpfile=$(create_temp)
cat $fp > $tmpfile
jq --arg VERSION "$ver" '.version = $VERSION' $tmpfile > $fp
}
update_AMO_info() {
local summary=$(cat ./manifest.json | jq -r '.description')
local description=$(< ./README.md)
local support_url="https://github.com/igorlogius/webextensions/issues/new/choose?title=$GITHUB_WORKFLOW%20-"
local template='{
"summary": {
"en-US": ""
},
"description": {
"en-US": ""
},
"support_url": {
"en-US": ""
}
}'
local updated_json=$(echo "$template" | jq --arg desciption "$description" --arg summary "$summary" --arg support_url "$support_url" '.description."en-US" = $desciption | .summary."en-US" = $summary | .support_url."en-US" = $support_url' )
# note: we need to use the v5 API for this
# https://mozilla.github.io/addons-server/topics/api/addons.html#edit
# This endpoint allows an add-on’s AMO metadata to be edited.
local outfile=$(create_temp);
local status=$(curl \
-s \
-L \
--url "$BASE_URL/api/v5/addons/addon/$GITHUB_WORKFLOW/" \
-X PATCH \
-H 'Content-type: application/json' \
-H "Authorization: JWT $(generate_auth_token)" \
-d "$updated_json" \
-w "%{http_code}" \
-o $outfile
)
if [ $status -ne 200 ];then
echo ""
cat $outfile
echo ""
echo "ERROR: failed to update AMO infos $url got status: $status"
exit 1
fi
}
##
## MAIN
##
# testing
#GITHUB_REPOSITORY_OWNER=igorlogius
#GITHUB_WORKFLOW=copy-tabs
check_vars GITHUB_REPOSITORY_OWNER GITHUB_WORKFLOW
check_deps stat cat mktemp date zip curl jq
# create and move to empty workdir
WORKDIR=$(create_temp --dir)
cd $WORKDIR;
# get VERSION + GUID
IFS=' ' read -r AMO_VERSION AMO_GUID <<<$(get_remote_content "$AMO_API_URL/addon/$GITHUB_WORKFLOW" | jq -r '.current_version.version, .guid' | tr '\n' ' ')
# determine next version
NEXT_AMO_VERSION=$(get_next_version "$AMO_VERSION")
# bulk download via curl -K with jq format magic also excludes README.md
curl -s -K <(get_remote_content "https://api.github.com/repos/$GITHUB_REPOSITORY_OWNER/webextensions/contents/sources/$GITHUB_WORKFLOW" | jq -r '.[] | select ( .type == "file") | "url = \(.download_url)\noutput = \(.name)"')
# replace module symlinks with real content
for file in *;do
SIZE=$( stat -c "%s" "$file" )
if [ $SIZE -gt 14 ]; then
TMP=$( head -c 14 "$file" | tr -d '\0' )
if [ "$TMP" = "../../modules/" ]; then
get_remote_content "https://raw.githubusercontent.com/$GITHUB_REPOSITORY_OWNER/webextensions/main/modules/$file" > $file
fi
fi
done
# write new version to file
update_version "manifest.json" "${NEXT_AMO_VERSION}"
# pack files
XPIFILE="${GITHUB_WORKFLOW}-${NEXT_AMO_VERSION}-$(date '+%F_%H-%M').zip"
zip -r "$XPIFILE" *
# # #
check_vars ISSUER SECRET
check_deps base64 openssl
upload_xpi "$AMO_GUID" "$NEXT_AMO_VERSION" "$XPIFILE"
#lets wait a few minutes because the AMO API is easily overwhelmed
sleep 5m
# we need to use the v5 API for this
update_AMO_info