From 312fe6adc852cceaa99927e0dc3a19a6d0abb75e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jul 2023 12:01:50 +0200 Subject: [PATCH 1/5] Compute version code and version name from separate component --- plugins/src/main/kotlin/Versions.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 3a6c591e19..a3dfa86de9 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -17,10 +17,18 @@ import org.gradle.api.JavaVersion import org.gradle.jvm.toolchain.JavaLanguageVersion -object Versions { - const val versionCode = 100200 - const val versionName = "0.2.0" +// Note: 2 digits max for each value +private const val versionMajor = 0 +private const val versionMinor = 1 + +// Note: even values are reserved for regular release, odd values for hotfix release. +// When creating a hotfix, you should decrease the value, since the current value +// is the value for the next regular release. +private const val versionPatch = 2 +object Versions { + val versionCode = (versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch) * 10 + val versionName = "$versionMajor.$versionMinor.$versionPatch" const val compileSdk = 33 const val targetSdk = 33 const val minSdk = 23 From 3f399ff396bf31db17de6c97a674303da873bad5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jul 2023 13:20:43 +0200 Subject: [PATCH 2/5] Add release script --- tools/release/download_github_artifacts.py | 154 ++++++++++ tools/release/release.sh | 318 +++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100755 tools/release/download_github_artifacts.py create mode 100755 tools/release/release.sh diff --git a/tools/release/download_github_artifacts.py b/tools/release/download_github_artifacts.py new file mode 100755 index 0000000000..892a4affa6 --- /dev/null +++ b/tools/release/download_github_artifacts.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import argparse +import hashlib +import json +import os +# Run `pip3 install requests` if not installed yet +import requests + +# This script downloads artifacts from GitHub. +# Ref: https://docs.github.com/en/rest/actions/artifacts#get-an-artifact + +error = False + +### Arguments + +parser = argparse.ArgumentParser(description='Download artifacts from GitHub.') +parser.add_argument('-t', + '--token', + required=True, + help='The GitHub token with read access.') +parser.add_argument('-a', + '--artifactUrl', + required=True, + help='the artifact_url from GitHub.') +parser.add_argument('-f', + '--filename', + help='the filename, if not provided, will use the artifact name.') +parser.add_argument('-i', + '--ignoreErrors', + help='Ignore errors that can be ignored. Build state and number of artifacts.', + action="store_true") +parser.add_argument('-d', + '--directory', + default="", + help='the target directory, where files will be downloaded. If not provided the build number will be used to create a directory.') +parser.add_argument('-v', + '--verbose', + help="increase output verbosity.", + action="store_true") +parser.add_argument('-s', + '--simulate', + help="simulate action, do not create folder or download any file.", + action="store_true") + +args = parser.parse_args() + +if args.verbose: + print("Argument:") + print(args) + +# Split the artifact URL to get information +# Ex: https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121 +artifactUrl = args.artifactUrl +if not artifactUrl.startswith('https://github.com/'): + print("❌ Invalid parameter --artifactUrl %s. Must start with 'https://github.com/'" % artifactUrl) + exit(1) +if "/artifacts/" not in artifactUrl: + print("❌ Invalid parameter --artifactUrl %s. Must contain '/artifacts/'" % artifactUrl) + exit(1) +artifactItems = artifactUrl.split("/") +if len(artifactItems) != 9: + print("❌ Invalid parameter --artifactUrl %s. Please check the format." % (artifactUrl)) + exit(1) + +gitHubRepoOwner = artifactItems[3] +gitHubRepo = artifactItems[4] +artifactId = artifactItems[8] + +if args.verbose: + print("gitHubRepoOwner: %s, gitHubRepo: %s, artifactId: %s" % (gitHubRepoOwner, gitHubRepo, artifactId)) + +headers = { + 'Authorization': "Bearer %s" % args.token, + 'Accept': 'application/vnd.github+json' +} +base_url = "https://api.github.com/repos/%s/%s/actions/artifacts/%s" % (gitHubRepoOwner, gitHubRepo, artifactId) + +### Fetch build state + +print("Getting artifacts data of project '%s/%s' artifactId '%s'..." % (gitHubRepoOwner, gitHubRepo, artifactId)) + +if args.verbose: + print("Url: %s" % base_url) + +r = requests.get(base_url, headers=headers) +data = json.loads(r.content.decode()) + +if args.verbose: + print("Json data:") + print(data) + +if args.verbose: + print("Create subfolder %s to download artifacts..." % artifactId) + +if args.directory == "": + targetDir = artifactId +else: + targetDir = args.directory + +if not args.simulate: + os.makedirs(targetDir, exist_ok=True) + +url = data.get("archive_download_url") +if args.filename is not None: + filename = args.filename +else: + filename = data.get("name") + ".zip" + +## Print some info about the artifact origin +commitLink = "https://github.com/%s/%s/commit/%s" % (gitHubRepoOwner, gitHubRepo, data.get("workflow_run").get("head_sha")) +print("Preparing to download artifact `%s`, built from branch: `%s` (commit %s)" % (data.get("name"), data.get("workflow_run").get("head_branch"), commitLink)) + +if args.verbose: + print() + print("Artifact url: %s" % url) + +target = targetDir + "/" + filename +sizeInBytes = data.get("size_in_bytes") +print("Downloading %s to '%s' (file size is %s bytes, this may take a while)..." % (filename, targetDir, sizeInBytes)) +if not args.simulate: + # open file to write in binary mode + with open(target, "wb") as file: + # get request + response = requests.get(url, headers=headers) + # write to file + file.write(response.content) + print("Verifying file size...") + # get the file size + size = os.path.getsize(target) + if sizeInBytes != size: + # error = True + print("Warning, file size mismatch: expecting %s and get %s. This is just a warning for now..." % (sizeInBytes, size)) + +if error: + print("❌ Error(s) occurred, please check the log") + exit(1) +else: + print("Done!") diff --git a/tools/release/release.sh b/tools/release/release.sh new file mode 100755 index 0000000000..4c8a291639 --- /dev/null +++ b/tools/release/release.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash + +# +# Copyright (c) 2023 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +printf "\n================================================================================\n" +printf "| Welcome to the release script! |\n" +printf "================================================================================\n" + +printf "Checking environment...\n" +envError=0 + +# Check that bundletool is installed +if ! command -v bundletool &> /dev/null +then + printf "Fatal: bundletool is not installed. You can install it running \`brew install bundletool\`\n" + envError=1 +fi + +# Path of the key store (it's a file) +keyStorePath="${ELEMENT_X_KEYSTORE_PATH}" +if [[ -z "${keyStorePath}" ]]; then + printf "Fatal: ELEMENT_X_KEYSTORE_PATH is not defined in the environment.\n" + envError=1 +fi +# Keystore password +keyStorePassword="${ELEMENT_X_KEYSTORE_PASSWORD}" +if [[ -z "${keyStorePassword}" ]]; then + printf "Fatal: ELEMENT_X_KEYSTORE_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# Key password +keyPassword="${ELEMENT_X_KEY_PASSWORD}" +if [[ -z "${keyPassword}" ]]; then + printf "Fatal: ELEMENT_X_KEY_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# GitHub token +gitHubToken="${ELEMENT_GITHUB_TOKEN}" +if [[ -z "${gitHubToken}" ]]; then + printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n" + envError=1 +fi +# Android home +androidHome="${ANDROID_HOME}" +if [[ -z "${androidHome}" ]]; then + printf "Fatal: ANDROID_HOME is not defined in the environment.\n" + envError=1 +fi +# @elementbot:matrix.org matrix token / Not mandatory +elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}" +if [[ -z "${elementBotToken}" ]]; then + printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n" +fi + +if [ ${envError} == 1 ]; then + exit 1 +fi + +minSdkVersion=23 +buildToolsVersion="32.0.0" +buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" + +if [[ ! -d ${buildToolsPath} ]]; then + printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n" + exit 1 +fi + +# Check if git flow is enabled +git flow config >/dev/null 2>&1 +if [[ $? == 0 ]] +then + printf "Git flow is initialized\n" +else + printf "Git flow is not initialized. Initializing...\n" + # All default value, just set 'v' for tag prefix + git flow init -d -t 'v' +fi + +printf "OK\n" + +printf "\n================================================================================\n" +printf "Ensuring main and develop branches are up to date...\n" + +git checkout main +git pull +git checkout develop +git pull + +printf "\n================================================================================\n" +# Guessing version to propose a default version +versionsFile="./plugins/src/main/kotlin/Versions.kt" +versionMajorCandidate=`grep "val versionMajor" ${versionsFile} | cut -d " " -f6` +versionMinorCandidate=`grep "val versionMinor" ${versionsFile} | cut -d " " -f6` +versionPatchCandidate=`grep "val versionPatch" ${versionsFile} | cut -d " " -f6` +versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}" + +read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version +version=${version:-${versionCandidate}} + +# extract major, minor and patch for future use +versionMajor=`echo ${version} | cut -d "." -f1` +versionMinor=`echo ${version} | cut -d "." -f2` +versionPatch=`echo ${version} | cut -d "." -f3` +nextPatchVersion=$((versionPatch + 2)) + +printf "\n================================================================================\n" +printf "Starting the release ${version}\n" +git flow release start ${version} + +# Note: in case the release is already started and the script is started again, checkout the release branch again. +ret=$? +if [[ $ret -ne 0 ]]; then + printf "Mmh, it seems that the release is already started. Checking out the release branch...\n" + git checkout "release/${version}" +fi + +# Ensure version is OK +versionsFileBak="${versionsFile}.bak" +cp ${versionsFile} ${versionsFileBak} +sed "s/private const val versionMajor = .*/private const val versionMajor = ${versionMajor}/" ${versionsFileBak} > ${versionsFile} +sed "s/private const val versionMinor = .*/private const val versionMinor = ${versionMinor}/" ${versionsFile} > ${versionsFileBak} +sed "s/private const val versionPatch = .*/private const val versionPatch = ${versionPatch}/" ${versionsFileBak} > ${versionsFile} +rm ${versionsFileBak} + +# This commit may have no effect because generally we do not change the version during the release. +git commit -a -m "Setting version for the release ${version}" + +printf "\n================================================================================\n" +printf "Building the bundle locally first...\n" +./gradlew clean bundleRelease + +printf "\n================================================================================\n" +printf "Running towncrier...\n" +yes | towncrier build --version "v${version}" + +printf "\n================================================================================\n" +read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter when it's done." + +# Get the changes to use it to create the GitHub release +changelogUrlEncoded=`git diff CHANGES.md | grep ^+ | tail -n +2 | cut -c2- | jq -sRr @uri | sed s/\(/%28/g | sed s/\)/%29/g` + +printf "\n================================================================================\n" +printf "Committing...\n" +git commit -a -m "Changelog for version ${version}" + +printf "\n================================================================================\n" +printf "Creating fastlane file...\n" +printf -v versionMajor2Digits "%02d" ${versionMajor} +printf -v versionMinor2Digits "%02d" ${versionMinor} +printf -v versionPatch2Digits "%02d" ${versionPatch} +fastlaneFile="4${versionMajor2Digits}${versionMinor2Digits}${versionPatch2Digits}0.txt" +fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}" +printf "Main changes in this version: TODO.\nFull changelog: https://github.com/vector-im/element-x-android/releases" > ${fastlanePathFile} + +read -p "I have created the file ${fastlanePathFile}, please edit it and press enter when it's done." +git add ${fastlanePathFile} +git commit -a -m "Adding fastlane file for version ${version}" + +printf "\n================================================================================\n" +printf "OK, finishing the release...\n" +git flow release finish "${version}" + +printf "\n================================================================================\n" +read -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush +doPush=${doPush:-yes} + +if [ ${doPush} == "yes" ]; then + printf "Pushing branch 'main' and tag 'v${version}'...\n" + git push origin main + git push origin "v${version}" +else + printf "Not pushing, do not forget to push manually!\n" +fi + +printf "\n================================================================================\n" +printf "Checking out develop...\n" +git checkout develop + +# Set next version +printf "\n================================================================================\n" +printf "Setting next version on file '${versionsFile}'...\n" +cp ${versionsFile} ${versionsFileBak} +sed "s/private const val versionPatch = .*/private const val versionPatch = ${nextPatchVersion}/" ${versionsFileBak} > ${versionsFile} +rm ${versionsFileBak} + +printf "\n================================================================================\n" +read -p "I have updated the versions to prepare the next release, please check that the change are correct and press enter so I can commit." + +printf "Committing...\n" +git commit -a -m 'version++' + +printf "\n================================================================================\n" +read -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits) " doPush +doPush=${doPush:-yes} + +if [ ${doPush} == "yes" ]; then + printf "Pushing branch 'develop'...\n" + git push origin develop +else + printf "Not pushing, do not forget to push manually!\n" +fi + +printf "\n================================================================================\n" +printf "Wait for the GitHub action https://github.com/vector-im/element-x-android/actions/workflows/release.yml?query=branch%%3Amain to build the 'main' branch.\n" +read -p "After GHA is finished, please enter the artifact URL (for 'elementx-app-bundle-unsigned'): " artifactUrl + +printf "\n================================================================================\n" +printf "Downloading the artifact...\n" + + # Download files +targetPath="./tmp/Element/${version}" + +python3 ./tools/release/download_github_artifacts.py \ + --token ${gitHubToken} \ + --artifactUrl ${artifactUrl} \ + --directory ${targetPath} \ + --ignoreErrors + +printf "\n================================================================================\n" +printf "Unzipping the artifact...\n" + +unzip ${targetPath}/elementx-app-bundle-unsigned.zip -d ${targetPath} + +unsignedBundlePath="${targetPath}/app-release.aab" +signedBundlePath="${targetPath}/app-release-signed.aab" + +printf "\n================================================================================\n" +printf "Signing file ${unsignedBundlePath} with build-tools version ${buildToolsVersion} for min SDK version ${minSdkVersion}...\n" + +cp ${unsignedBundlePath} ${signedBundlePath} + +${buildToolsPath}/apksigner sign \ + -v \ + --ks ${keyStorePath} \ + --ks-pass pass:${keyStorePassword} \ + --ks-key-alias elementx \ + --key-pass pass:${keyPassword} \ + --min-sdk-version ${minSdkVersion} \ + ${signedBundlePath} + +printf "\n================================================================================\n" +printf "Please check the information below:\n" + +printf "Version code: " +bundletool dump manifest --bundle=${signedBundlePath} --xpath=/manifest/@android:versionCode +printf "Version name: " +bundletool dump manifest --bundle=${signedBundlePath} --xpath=/manifest/@android:versionName + +printf "\n" +read -p "Does it look correct? Press enter when it's done." + +printf "\n================================================================================\n" +printf "The file ${signedBundlePath} has been signed and can be uploaded to the PlayStore!\n" + +printf "\n================================================================================\n" +read -p "Do you want to install the application to your device? Make sure there is a connected device first. (yes/no) default to yes " doDeploy +doDeploy=${doDeploy:-yes} + +if [ ${doDeploy} == "yes" ]; then + printf "Building apks...\n" + bundletool build-apks --bundle=${signedBundlePath} --output=${targetPath}/elementx.apks \ + --ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \ + --overwrite + printf "Installing apk for your device...\n" + bundletool install-apks --apks=${targetPath}/elementx.apks + read -p "Please run the application on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." +else + printf "Apk will not be deployed!\n" +fi + +printf "\n================================================================================\n" +githubCreateReleaseLink="https://github.com/vector-im/element-x-android/releases/new?tag=v${version}&title=Element%20X%20Android%20v${version}&body=${changelogUrlEncoded}" +printf "Creating the release on gitHub.\n" +printf -- "Open this link: %s\n" ${githubCreateReleaseLink} +printf "Then\n" +printf " - copy paste the section of the file CHANGES.md for this release (if not there yet)\n" +printf " - click on the 'Generate releases notes' button\n" +printf " - Add the file ${signedBundlePath} to the GitHub release.\n" +read -p ". Press enter when it's done. " + +printf "\n================================================================================\n" +printf "Message for the Android internal room:\n\n" +message="@room Element X Android ${version} is ready to be tested. You can get it from https://github.com/vector-im/element-x-android/releases/tag/v${version}. Please report any feedback here. Thanks!" +printf "${message}\n\n" + +if [[ -z "${elementBotToken}" ]]; then + read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " +else + read -p "Send this message to the room (yes/no) default to yes? " doSend + doSend=${doSend:-yes} + if [ ${doSend} == "yes" ]; then + printf "Sending message...\n" + transactionId=`openssl rand -hex 16` + # Element Android internal + matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org" + curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId} + else + printf "Message not sent, please send it manually!\n" + fi +fi + +printf "\n================================================================================\n" +printf "Congratulation! Kudos for using this script! Have a nice day!\n" +printf "================================================================================\n" From 8833bd09d6b420801009e7213bff872a1a47c908 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jul 2023 19:15:56 +0200 Subject: [PATCH 3/5] Extract git flow init from release script (so that it can be run standalone) and do not use `-t` which does not seem to be standard. --- tools/gitflow/gitflow-init.sh | 19 +++++++++++++++++++ tools/release/release.sh | 3 +-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100755 tools/gitflow/gitflow-init.sh diff --git a/tools/gitflow/gitflow-init.sh b/tools/gitflow/gitflow-init.sh new file mode 100755 index 0000000000..5068a37575 --- /dev/null +++ b/tools/gitflow/gitflow-init.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# +# Copyright (c) 2023 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +git flow init -d +git config gitflow.prefix.versiontag v diff --git a/tools/release/release.sh b/tools/release/release.sh index 4c8a291639..e51ce259c0 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -86,8 +86,7 @@ then printf "Git flow is initialized\n" else printf "Git flow is not initialized. Initializing...\n" - # All default value, just set 'v' for tag prefix - git flow init -d -t 'v' + ./tools/gitflow/gitflow-init.sh fi printf "OK\n" From 3e2a516b94e5b83071d53f35f57e22d2547ecb49 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jul 2023 19:22:53 +0200 Subject: [PATCH 4/5] Move download_github_artifacts.py to its own folder. --- tools/{release => github}/download_github_artifacts.py | 0 tools/release/release.sh | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tools/{release => github}/download_github_artifacts.py (100%) diff --git a/tools/release/download_github_artifacts.py b/tools/github/download_github_artifacts.py similarity index 100% rename from tools/release/download_github_artifacts.py rename to tools/github/download_github_artifacts.py diff --git a/tools/release/release.sh b/tools/release/release.sh index e51ce259c0..60955e1f94 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -223,7 +223,7 @@ printf "Downloading the artifact...\n" # Download files targetPath="./tmp/Element/${version}" -python3 ./tools/release/download_github_artifacts.py \ +python3 ./tools/github/download_github_artifacts.py \ --token ${gitHubToken} \ --artifactUrl ${artifactUrl} \ --directory ${targetPath} \ From b98d57c9c82a2c01067fa2b32b1f5ac5e0e801d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jul 2023 19:40:57 +0200 Subject: [PATCH 5/5] Change how we detect if git flow is init. --- tools/release/release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/release/release.sh b/tools/release/release.sh index 60955e1f94..54ee8117ec 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -80,8 +80,8 @@ if [[ ! -d ${buildToolsPath} ]]; then fi # Check if git flow is enabled -git flow config >/dev/null 2>&1 -if [[ $? == 0 ]] +gitFlowDevelop=`git config gitflow.branch.develop` +if [[ ${gitFlowDevelop} != "" ]] then printf "Git flow is initialized\n" else