diff --git a/.gitignore b/.gitignore index 3cddb83e86..599ebb7658 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ status-go-*.zip ios/Pods ios/StatusIm.xcworkspace .ruby-version +status-e2e/ #python *.pyc diff --git a/ci/Jenkinsfile.combined b/ci/Jenkinsfile.combined index d76fe0f5a1..de8c430163 100644 --- a/ci/Jenkinsfile.combined +++ b/ci/Jenkinsfile.combined @@ -26,45 +26,44 @@ pipeline { } stage('Build') { parallel { - stage('MacOS') { - when { expression { btype != 'release' } } + stage('MacOS') { when { expression { btype != 'release' } } steps { script { osx = cmn.buildBranch('status-react/combined/desktop-macos') } } } - stage('Linux') { - when { expression { btype != 'release' } } + stage('Linux') { when { expression { btype != 'release' } } steps { script { nix = cmn.buildBranch('status-react/combined/desktop-linux') } } } - stage('Windows') { - when { expression { btype != 'release' } } + stage('Windows') { when { expression { btype != 'release' } } steps { script { win = cmn.buildBranch('status-react/combined/desktop-windows') } } } stage('iOS') { steps { script { ios = cmn.buildBranch('status-react/combined/mobile-ios') } } } + stage('iOS e2e') { steps { script { + iose2e = cmn.buildBranch('status-react/combined/mobile-ios', 'e2e') + } } } stage('Android') { steps { script { - dro = cmn.buildBranch('status-react/combined/mobile-android') + apk = cmn.buildBranch('status-react/combined/mobile-android') } } } stage('Android e2e') { steps { script { - e2e = cmn.buildBranch('status-react/combined/mobile-android', 'e2e') + apke2e = cmn.buildBranch('status-react/combined/mobile-android', 'e2e') } } } } } stage('Archive') { steps { script { sh('rm -f pkg/*') - if (btype != 'release') { cmn.copyArts('status-react/combined/desktop-macos', osx.number) cmn.copyArts('status-react/combined/desktop-linux', nix.number) cmn.copyArts('status-react/combined/desktop-windows', win.number) } cmn.copyArts('status-react/combined/mobile-ios', ios.number) - cmn.copyArts('status-react/combined/mobile-android', dro.number) - cmn.copyArts('status-react/combined/mobile-android', e2e.number) - cmn.copyArts('status-react/combined/mobile-ios', ios.number) + cmn.copyArts('status-react/combined/mobile-ios', iose2e.number) + cmn.copyArts('status-react/combined/mobile-android', apk.number) + cmn.copyArts('status-react/combined/mobile-android', apke2e.number) dir('pkg') { /* generate sha256 checksums for upload */ sh "sha256sum * | tee ${cmn.pkgFilename(btype, 'sha256')}" @@ -74,7 +73,7 @@ pipeline { } stage('Upload') { steps { script { - e2eUrl = cmn.uploadArtifact(cmn.pkgFind('e2e.apk')) + apke2eUrl = cmn.uploadArtifact(cmn.pkgFind('e2e.apk')) apkUrl = cmn.uploadArtifact(cmn.pkgFind("${btype}.apk")) if (btype != 'release') { dmgUrl = cmn.uploadArtifact(cmn.pkgFind('dmg')) @@ -85,18 +84,23 @@ pipeline { appUrl = null exeUrl = null } + iose2eUrl = cmn.uploadArtifact(cmn.pkgFind("e2e.app.zip")) /* special case for iOS Diawi links */ ipaUrl = ios.getBuildVariables().get('DIAWI_URL') /* upload the sha256 checksums file too */ shaUrl = cmn.uploadArtifact(cmn.pkgFind('sha256')) /* add URLs to the build description */ cmn.setBuildDesc( - Apk: apkUrl, e2e: e2eUrl, iOS: ipaUrl, App: appUrl, Mac: dmgUrl, Win: exeUrl, + Apk: apkUrl, Apke2e: apke2eUrl, + iOS: ipaUrl, iOSe2e: iose2eUrl, + App: appUrl, Mac: dmgUrl, Win: exeUrl, ) /* Create latest.json with newest nightly URLs */ if (btype == 'nightly') { cmn.updateLatestNightlies( - APK: apkUrl, IOS: ipaUrl, APP: appUrl, MAC: dmgUrl, WIN: exeUrl, SHA: shaUrl + APK: apkUrl, IOS: ipaUrl, + APP: appUrl, MAC: dmgUrl, + WIN: exeUrl, SHA: shaUrl ) } } } @@ -105,7 +109,9 @@ pipeline { steps { script { if (env.CHANGE_ID != null) { cmn.githubNotify( - apk: apkUrl, e2e: e2eUrl, ipa: ipaUrl, app: appUrl, dmg: dmgUrl, win: exeUrl, + apk: apkUrl, apke2e: apke2eUrl, + ipa: ipaUrl, iose2e: iose2eUrl, + app: appUrl, dmg: dmgUrl, win: exeUrl, ) } } } @@ -130,7 +136,7 @@ pipeline { stage('Run e2e') { when { expression { btype == 'nightly' } } steps { script { - e2eApk = e2e.getBuildVariables().get('SAUCE_URL') + e2eApk = apke2e.getBuildVariables().get('SAUCE_URL') build( job: 'end-to-end-tests/status-app-nightly', wait: false, parameters: [string(name: 'apk', value: "--apk=${e2eApk}")] diff --git a/ci/Jenkinsfile.ios b/ci/Jenkinsfile.ios index aae6dea986..f3cc24479b 100644 --- a/ci/Jenkinsfile.ios +++ b/ci/Jenkinsfile.ios @@ -1,5 +1,5 @@ pipeline { - agent { label 'fastlane' } + agent { label 'macos' } options { timestamps() @@ -7,8 +7,8 @@ pipeline { timeout(time: 35, unit: 'MINUTES') /* Limit builds retained */ buildDiscarder(logRotator( - numToKeepStr: '60', - daysToKeepStr: '30', + numToKeepStr: '90', + daysToKeepStr: '60', artifactNumToKeepStr: '60', )) } @@ -66,7 +66,14 @@ pipeline { } stage('Upload') { steps { - script { env.DIAWI_URL = mobile.ios.uploadToDiawi() } + script { + switch (cmn.getBuildType()) { + case 'nightly': + env.DIAWI_URL = mobile.ios.uploadToDiawi(); break; + case 'e2e': + env.SAUCE_URL = mobile.ios.uploadToSauceLabs(); break; + } + } } } } diff --git a/ci/android.groovy b/ci/android.groovy index 3fd38f28ad..083ff56e63 100644 --- a/ci/android.groovy +++ b/ci/android.groovy @@ -26,9 +26,9 @@ def uploadToPlayStore(type = 'nightly') { def uploadToSauceLabs() { def changeId = common.getParentRunEnv('CHANGE_ID') if (changeId != null) { - env.SAUCE_LABS_APK = "${changeId}.apk" + env.SAUCE_LABS_NAME = "${changeId}.apk" } else { - env.SAUCE_LABS_APK = "im.status.ethereum-e2e-${GIT_COMMIT.take(6)}.apk" + env.SAUCE_LABS_NAME = "im.status.ethereum-e2e-${GIT_COMMIT.take(6)}.apk" } withCredentials([ string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'SAUCE_ACCESS_KEY'), @@ -36,11 +36,11 @@ def uploadToSauceLabs() { ]) { sh 'bundle exec fastlane android saucelabs' } - return env.SAUCE_LABS_APK + return env.SAUCE_LABS_NAME } def uploadToDiawi() { - env.SAUCE_LABS_APK = "im.status.ethereum-e2e-${GIT_COMMIT.take(6)}.apk" + env.SAUCE_LABS_NAME = "im.status.ethereum-e2e-${GIT_COMMIT.take(6)}.apk" withCredentials([ string(credentialsId: 'diawi-token', variable: 'DIAWI_TOKEN'), ]) { diff --git a/ci/common.groovy b/ci/common.groovy index 92cfca9bf0..63050a3b26 100644 --- a/ci/common.groovy +++ b/ci/common.groovy @@ -144,7 +144,7 @@ def githubNotify(Map urls) { message += "CI BUILD SUCCESSFUL in ${currentBuild.durationString} (${GIT_COMMIT})\n" message += '| | | | | |\n' message += '|-|-|-|-|-|\n' - message += "| [Android](${urls.apk})([e2e](${urls.e2e})) | [iOS](${urls.ipa}) |" + message += "| [Android](${urls.apk}) ([e2e](${urls.apke2e})) | [iOS](${urls.ipa}) ([e2e](${urls.iose2e})) |" if (dmgUrl != null) { message += " [MacOS](${urls.dmg}) | [AppImage](${urls.app}) | [Windows](${urls.win}) |" } else { @@ -163,7 +163,13 @@ def githubNotify(Map urls) { } def pkgFind(glob) { - return findFiles(glob: "pkg/*${glob}")[0].path + def fullGlob = "pkg/*${glob}" + def found = findFiles(glob: fullGlob) + if (found.size() == 0) { + sh 'ls -l pkg/' + error("File not found via glob: ${fullGlob} ${found.size()}") + } + return found[0].path } def setBuildDesc(Map links) { diff --git a/ci/ios.groovy b/ci/ios.groovy index bb52c3507d..d80f2a76a0 100644 --- a/ci/ios.groovy +++ b/ci/ios.groovy @@ -1,24 +1,23 @@ -common = load('ci/common.groovy') +cmn = load('ci/common.groovy') def plutil(name, value) { sh "plutil -replace ${name} -string ${value} ios/StatusIm/Info.plist" } def compile(type = 'nightly') { - def target = 'nightly' - - if (type == 'release') { - target = 'adhoc' + def target + switch (type) { + case 'release': target = 'adhoc'; break; + case 'testflight': target = 'release'; break; + case 'e2e': target = 'e2e'; break; + default: target = 'nightly'; } - - if (type == 'testflight') { - target = 'release' - } - /* configure build metadata */ plutil('CFBundleShortVersionString', common.version()) plutil('CFBundleVersion', common.buildNumber()) plutil('CFBundleBuildUrl', currentBuild.absoluteUrl) + /* the dir might not exist */ + sh 'mkdir -p status-e2e' /* build the actual app */ withCredentials([ string(credentialsId: 'SLACK_URL', variable: 'SLACK_URL'), @@ -29,12 +28,16 @@ def compile(type = 'nightly') { ]) { sh "bundle exec fastlane ios ${target}" } - if (type != 'testflight') { - def pkg = common.pkgFilename(type, 'ipa') - sh "cp status-adhoc/StatusIm.ipa ${pkg}" - return pkg + /* rename built file for uploads and archivization */ + def pkg = '' + if (type == 'e2e') { + pkg = cmn.pkgFilename('e2e', 'app.zip') + sh "cp status-e2e/StatusIm.app.zip ${pkg}" + } else if (type != 'testflight') { + pkg = cmn.pkgFilename(type, 'ipa') + sh "cp status-adhoc/StatusIm.ipa ${pkg}" } - return '' + return pkg } def uploadToDiawi() { @@ -47,4 +50,20 @@ def uploadToDiawi() { return diawiUrl } +def uploadToSauceLabs() { + def changeId = cmn.getParentRunEnv('CHANGE_ID') + if (changeId != null) { + env.SAUCE_LABS_NAME = "${changeId}.app.zip" + } else { + env.SAUCE_LABS_NAME = "im.status.ethereum-e2e-${cmn.gitCommit()}.app.zip" + } + withCredentials([ + string(credentialsId: 'SAUCE_ACCESS_KEY', variable: 'SAUCE_ACCESS_KEY'), + string(credentialsId: 'SAUCE_USERNAME', variable: 'SAUCE_USERNAME'), + ]) { + sh 'bundle exec fastlane ios saucelabs' + } + return env.SAUCE_LABS_NAME +} + return this diff --git a/ci/mobile.groovy b/ci/mobile.groovy index 20752efc76..854e79326e 100644 --- a/ci/mobile.groovy +++ b/ci/mobile.groovy @@ -27,7 +27,7 @@ def podUpdate() { try { wait(lockFile) sh "touch ${lockFile}" - sh 'pod update --silent' + sh 'pod update --silent --no-ansi' } finally { sh "rm ${lockFile}" } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c1dc94f3ba..8cc1a0afd1 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -27,9 +27,9 @@ end # uploads `file` to sauce labs (overwrites if there is anoter file from the # same commit) def upload_to_saucelabs(file) - username = ENV["SAUCE_USERNAME"] key = ENV["SAUCE_ACCESS_KEY"] - unique_name = ENV["SAUCE_LABS_APK"] + username = ENV["SAUCE_USERNAME"] + unique_name = ENV["SAUCE_LABS_NAME"] url = "https://saucelabs.com/rest/v1/storage/" + username + '/' + unique_name + "?overwrite=true" @@ -71,6 +71,50 @@ def build_ios_adhoc ) end +# builds an ios app with e2e configuration and put it +# to "status-e2e" output folder +def build_ios_e2e + + # determine a simulator SDK installed + showsdks_output = sh('xcodebuild', '-showsdks') + simulator_sdk = showsdks_output.scan(/iphonesimulator\d\d?\.\d\d?/).first + + match( + type: "adhoc", + force_for_new_devices: true, + readonly: true, + keychain_name: "login.keychain" + ) + + + build_ios_app( + # Creating a build for the iOS Simulator + # 1. https://medium.com/rocket-fuel/fastlane-to-the-simulator-87549b2601b9 + sdk: simulator_sdk, + destination: "generic/platform=iOS Simulator", + # 2. fixing compilations issues as stated in https://stackoverflow.com/a/20505258 + # it looks like i386 isn't supported by React Native + xcargs: "ARCHS=\"x86_64\" ONLY_ACTIVE_ARCH=NO", + # 3. directory where to up StatusIm.app + derived_data_path: "status-e2e", + output_name: "StatusIm.app", + # ------------------------------------- + # Normal stuff + scheme: "StatusIm", + workspace: "ios/StatusIm.xcworkspace", + configuration: "Release", + # Simulator apps can't be archived... + skip_archive: true, + # ...and we don't need an .ipa file for them, because we use .app directly + skip_package_ipa: true + ) + + zip( + path: "status-e2e/Build/Products/Release-iphonesimulator/StatusIm.app", + output_path: "status-e2e/StatusIm.app.zip", + verbose: false, + ) +end def upload_to_diawi(source) diawi( @@ -84,7 +128,7 @@ end platform :ios do desc "`fastlane ios adhoc` - ad-hoc lane for iOS." - desc "This lane is used PRs, Releases, etc." + desc "This lane is used for PRs, Releases, etc." desc "It creates an .ipa that can be used by a list of devices, registeded in the App Store Connect." desc "This .ipa is ready to be distibuted through diawi.com" lane :adhoc do @@ -92,6 +136,14 @@ platform :ios do build_ios_adhoc end + desc "`fastlane ios e2e` - e2e lane for iOS." + desc "This lane is used for SauceLabs end-to-end testing." + desc "It creates an .app that can be used inside of a iPhone simulator." + lane :e2e do + unlock_keychain_if_needed + build_ios_e2e + end + desc "`fastlane ios pr` - makes a new pr build" desc "This lane builds a new adhoc build and leaves an .ipa that is ad-hoc signed (can be uploaded to diawi)" lane :pr do @@ -149,6 +201,19 @@ platform :ios do upload_to_diawi("status-adhoc/StatusIm.ipa") end + desc "`fastlane ios saucelabs` - upload .app to sauce labs" + desc "also notifies in a GitHub comments" + desc "expects to have an .apk prepared: `android/app/build/outputs/apk/release/app-release.apk`" + desc "expects to have a saucelabs access key as SAUCE_ACCESS_KEY env variable" + desc "expects to have a saucelabs username token as SAUCE_USERNAME env variable" + desc "expects to have a saucelabs destination name as SAUCE_LABS_NAME env variable" + desc "will fails if file isn't there" + lane :saucelabs do + upload_to_saucelabs( + "status-e2e/StatusIm.app.zip" + ) + end + desc "This fastlane step cleans up XCode DerivedData folder" lane :cleanup do clear_derived_data @@ -204,7 +269,7 @@ platform :android do desc "expects to have an .apk prepared: `android/app/build/outputs/apk/release/app-release.apk`" desc "expects to have a saucelabs access key as SAUCE_ACCESS_KEY env variable" desc "expects to have a saucelabs username token as SAUCE_USERNAME env variable" - desc "expects to have a saucelabs destination name as SAUCE_LABS_APK env variable" + desc "expects to have a saucelabs destination name as SAUCE_LABS_NAME env variable" desc "will fails if file isn't there" lane :saucelabs do upload_to_saucelabs(