#!/bin/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 missing dependencies"; exit 1; } } check_vars(){ MISSING=0 for req in "$@";do [ -z $(eval echo "\${${req}+x}") ] && { echo "ERROR: missing variable: $req"; MISSING=1; } done [ $MISSING -ne 0 ] && { echo "ERROR: aborting missing dependencies"; 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 template='{ "summary": { "en-US": "" }, "description": { "en-US": "" } }' local updated_json=$(echo "$template" | jq --arg desciption "$description" --arg summary "$summary" '.description."en-US" = $desciption | .summary."en-US" = $summary') # 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 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/$GITHUB_WORKFLOW" | jq -r '.[] | select ( .type == "file") | "url = \(.download_url)\noutput = \(.name)"') # 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" # we need to use the v5 API for this update_AMO_info