diff --git a/app-ios/fastlane/Fastfile b/app-ios/fastlane/Fastfile index a873aea71fce..e4b8140fd418 100644 --- a/app-ios/fastlane/Fastfile +++ b/app-ios/fastlane/Fastfile @@ -16,8 +16,8 @@ default_platform(:ios) platform :ios do - desc "Push a new prod release to AppStore" - lane :appstore_prod do |options| + desc "Build a new Mail prod release with AppStore configuration" + lane :build_mail_prod do |options| match( app_identifier: ["de.tutao.tutanota", "de.tutao.tutanota.TutanotaShareExtension", "de.tutao.tutanota.TutanotaNotificationExtension"], type: "appstore", @@ -37,18 +37,22 @@ platform :ios do include_symbols: true, verbose: true ) - if options[:submit] - upload_to_app_store( - skip_screenshots: true, - submit_for_review: false, - precheck_include_in_app_purchases: false, - # must use force as long as we don't automatically create html previews - force: true, - api_key_path: ENV["API_KEY_JSON_FILE_PATH"] - ) - end end + desc "Publish a Mail artifact to AppStore" + lane :publish_mail_prod do |options| + sh 'echo "Uploading mail artifact ' + options[:file] + '"' + + upload_to_app_store( + skip_screenshots: true, + submit_for_review: false, + precheck_include_in_app_purchases: false, + # must use force as long as we don't automatically create html previews + force: true, + api_key_path: ENV["API_KEY_JSON_FILE_PATH"] + ) + end + desc "Build a new prod release for ad-hoc" lane :adhoc_prod do |options| match( @@ -77,7 +81,7 @@ platform :ios do desc "Push a new staging release to TestFlight" - lane :testflight_staging do + lane :build_mail_staging do match( app_identifier: ["de.tutao.tutanota.test", "de.tutao.tutanota.test.TutanotaShareExtension", "de.tutao.tutanota.test.TutanotaNotificationExtension"], type: "appstore", @@ -96,13 +100,18 @@ platform :ios do output_name: "tutanota-" + get_version_number(target: "tutanota") + "-test", verbose: true ) + end + + desc "Publish Mail staging to TestFlight" + lane :publish_mail_staging do |options| + sh 'echo "Uploading mail staging to TestFlight ' + options[:file] + '"' upload_to_testflight( app_identifier: "de.tutao.tutanota.test", skip_submission: true, api_key_path: ENV["API_KEY_JSON_FILE_PATH"] ) - end + end desc "Build a new staging release for ad-hoc" lane :adhoc_staging do diff --git a/buildSrc/createReleaseDraft.js b/buildSrc/createReleaseDraft.js index a51853585890..0792326bc022 100644 --- a/buildSrc/createReleaseDraft.js +++ b/buildSrc/createReleaseDraft.js @@ -29,17 +29,6 @@ if (wasRunFromCli) { } async function run({ name, tag, notes, uploadFile, dryRun, toFile }) { - const releaseToken = process.env.GITHUB_TOKEN - - if (!releaseToken) { - throw new Error("No GITHUB_TOKEN set!") - } - - const octokit = new Octokit({ - auth: releaseToken, - userAgent: "tuta-github-release-v0.0.1", - }) - notes = renderCompleteNotes({ notes: await fs.promises.readFile(notes, { encoding: "utf8" }), files: uploadFile }) if (toFile) { @@ -48,6 +37,17 @@ async function run({ name, tag, notes, uploadFile, dryRun, toFile }) { } else if (dryRun) { console.log(`dry run, so not creating draft with release notes\n\n${notes}\nand name ${name}, tag ${tag} \n ${uploadFile}`) } else { + const releaseToken = process.env.GITHUB_TOKEN + + if (!releaseToken) { + throw new Error("No GITHUB_TOKEN set!") + } + + const octokit = new Octokit({ + auth: releaseToken, + userAgent: "tuta-github-release-v0.0.1", + }) + const draftResponse = await createReleaseDraft(octokit, name, tag, notes) const { upload_url, id } = draftResponse.data diff --git a/ci/Android.Jenkinsfile b/ci/Android.Jenkinsfile index 49cf94df7125..c06f089aec76 100644 --- a/ci/Android.Jenkinsfile +++ b/ci/Android.Jenkinsfile @@ -5,7 +5,6 @@ pipeline { PATH = "${env.NODE_PATH}:${env.PATH}:/home/jenkins/emsdk/upstream/bin/:/home/jenkins/emsdk/:/home/jenkins/emsdk/upstream/emscripten" ANDROID_SDK_ROOT = "/opt/android-sdk-linux" ANDROID_HOME = "/opt/android-sdk-linux" - GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-android-release-${VERSION}" } agent { @@ -19,15 +18,8 @@ pipeline { parameters { booleanParam( name: 'RELEASE', defaultValue: false, - description: "Build a test and release version of the app. " + - "Uploads both to Nexus and creates a new release on google play, " + - "which must be manually published from play.google.com/console" + description: "Build a test and release version of the app. Uploads both to Nexus." ) - persistentText( - name: "releaseNotes", - defaultValue: "", - description: "release notes for this build" - ) } stages { @@ -124,26 +116,6 @@ pipeline { assetFilePath: "${WORKSPACE}/build/app-android/tutanota-app-tutao-releaseTest-${VERSION}.apk", fileExtension: 'apk' ) - - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to upload android test app to Play Store') { - // This doesn't publish to the main app on play store, - // instead it gets published to the hidden "tutanota-test" app - // this happens because the AppId is set to de.tutao.tutanota.test by the android build - // and play store knows which app to publish just based on the id - androidApkUpload( - googleCredentialsId: 'android-app-publisher-credentials', - apkFilesPattern: "build/app-android/tutanota-app-tutao-releaseTest-${VERSION}.apk", - trackName: 'internal', - rolloutPercentage: '100%', - recentChangeList: [ - [ - language: "en-US", - text : "see: ${GITHUB_RELEASE_PAGE}" - ] - ] - ) // androidApkUpload - } // catchError - } } } // stage testing @@ -163,49 +135,10 @@ pipeline { assetFilePath: "${WORKSPACE}/${filePath}", fileExtension: 'apk' ) - - androidApkUpload( - googleCredentialsId: 'android-app-publisher-credentials', - apkFilesPattern: "${filePath}", - trackName: 'production', - // Don't publish the app to users directly - // It will require manual intervention at play.google.com/console - rolloutPercentage: '0%', - recentChangeList: [ - [ - language: "en-US", - text : "see: ${GITHUB_RELEASE_PAGE}" - ] - ] - ) } } } // stage production } } - stage('Tag and publish release page') { - when { - expression { return params.RELEASE } - } - steps { - // Needed to upload it - unstash 'apk-production' - - script { - def filePath = "build/app-android/tutanota-app-tutao-release-${VERSION}.apk" - - writeFile file: "notes.txt", text: params.releaseNotes - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to create github release page for android') { - withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { - sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (Android)' \ - --tag 'tutanota-android-release-${VERSION}' \ - --uploadFile '${WORKSPACE}/${filePath}' \ - --notes notes.txt""" - } // withCredentials - } // catchError - sh "rm notes.txt" - } // script - } - } } } diff --git a/ci/Desktop.Jenkinsfile b/ci/Desktop.Jenkinsfile index 2908a83c4abe..b4da9f8a1f60 100644 --- a/ci/Desktop.Jenkinsfile +++ b/ci/Desktop.Jenkinsfile @@ -10,15 +10,25 @@ pipeline { parameters { booleanParam( - name: 'RELEASE', + name: 'UPLOAD', defaultValue: false, - description: "Prepare a release version (doesn't publish to production, this is done manually)" + description: "Upload built clients to Nexus" + ) + booleanParam( + name: 'WINDOWS', + defaultValue: false, + description: "Build Windows client" + ) + booleanParam( + name: 'MAC', + defaultValue: false, + description: "Build Mac client" + ) + booleanParam( + name: 'LINUX', + defaultValue: false, + description: "Build Linux client" ) - persistentText( - name: "releaseNotes", - defaultValue: "", - description: "release notes for this build" - ) } agent { @@ -26,6 +36,17 @@ pipeline { } stages { + stage("Checking params") { + steps { + script{ + if(!params.WINDOWS && !params.MAC && !params.LINUX) { + currentBuild.result = 'ABORTED' + error('No artifacts were selected.') + } + } + echo "Params OKAY" + } + } stage('Check Github') { steps { script { @@ -57,6 +78,7 @@ pipeline { } stage('Native modules') { + when { expression { return params.WINDOWS } } agent { label 'win-native' } @@ -73,6 +95,7 @@ pipeline { stage('Build desktop clients') { parallel { stage('Windows') { + when { expression { return params.WINDOWS } } environment { PATH = "${env.NODE_PATH}:${env.PATH}" } @@ -101,6 +124,7 @@ pipeline { } stage('Mac') { + when { expression { return params.MAC } } environment { PATH = "${env.NODE_MAC_PATH}:${env.PATH}" } @@ -117,14 +141,14 @@ pipeline { ]) { sh 'security unlock-keychain -p $FASTLANE_KEYCHAIN_PASSWORD' script { - def stage = params.RELEASE ? 'release' : 'prod' + def stage = params.UPLOAD ? 'release' : 'prod' sh ''' export APPLEID=${APPLEIDVAR}; export APPLEIDPASS=${APPLEIDPASSVAR}; export APPLETEAMID=${APPLETEAMIDVAR}; node desktop --existing --architecture universal --platform mac ''' + "${stage}" dir('artifacts') { - if (params.RELEASE) { + if (params.UPLOAD) { stash includes: 'desktop-test/*', name:'mac_installer_test' } stash includes: 'desktop/*', name:'mac_installer' @@ -135,6 +159,7 @@ pipeline { } stage('Linux') { + when { expression { return params.LINUX } } agent { dockerfile { filename 'linux-build.dockerfile' @@ -158,8 +183,7 @@ pipeline { } } - stage('Preparation for build deb and publish') { - when { expression { return params.RELEASE } } + stage('Preparation for sign clients and upload to Nexus') { agent { label 'master' } @@ -170,8 +194,8 @@ pipeline { } } } - stage('Build deb and publish') { - when { expression { return params.RELEASE } } + stage('Sign clients and upload to Nexus') { + when { expression { return params.UPLOAD } } agent { dockerfile { filename 'linux-build.dockerfile' @@ -181,99 +205,155 @@ pipeline { args "--network host -v /run:/run:rw,z -v /opt/repository:/opt/repository:rw,z --device=${env.DEVICE_PATH}" } // docker } - environment { PATH = "${env.NODE_PATH}:${env.PATH}" } - steps { - sh 'npm ci' - sh 'npm run build-packages' - sh 'rm -rf ./build/*' - - dir('build') { - unstash 'linux_installer' - unstash 'mac_installer' - unstash 'win_installer' - unstash 'linux_installer_test' - unstash 'mac_installer_test' - unstash 'win_installer_test' - } + environment { + PATH = "${env.NODE_PATH}:${env.PATH}" + } + stages { + stage('Preparation for sign and upload') { + steps { + sh 'npm ci' + sh 'npm run build-packages' + sh 'rm -rf ./build/*' + } + } + stage('Sign and upload') { + parallel { + stage('Windows') { + when { expression { return params.WINDOWS } } + steps { + dir('build') { + unstash 'win_installer' + unstash 'win_installer_test' + } - withCredentials([string(credentialsId: 'HSM_USER_PIN', variable: 'PW')]) { - sh '''export HSM_USER_PIN=${PW}; node buildSrc/signDesktopClients.js''' - } + withCredentials([string(credentialsId: 'HSM_USER_PIN', variable: 'PW')]) { + sh '''export HSM_USER_PIN=${PW}; node buildSrc/signDesktopClients.js''' + } - sh 'node buildSrc/publish.js desktop' - - script { // create release draft - def desktopLinux = "build/desktop/tutanota-desktop-linux.AppImage" - def desktopWin = "build/desktop/tutanota-desktop-win.exe" - def desktopMac = "build/desktop/tutanota-desktop-mac.dmg" - - writeFile file: "notes.txt", text: params.releaseNotes - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to create github release page for desktop') { - withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { - sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (Desktop)' \ - --tag 'tutanota-desktop-release-${VERSION}' \ - --uploadFile '${WORKSPACE}/${desktopLinux}' \ - --uploadFile '${WORKSPACE}/${desktopWin}' \ - --uploadFile '${WORKSPACE}/${desktopMac}' \ - --notes notes.txt""" - } // withCredentials - } // catchError - sh "rm notes.txt" - } // script release draft - - script { // upload to nexus - def util = load "ci/jenkins-lib/util.groovy" + uploadWindowsArtifacts() + } + } // windows + stage('Mac') { + when { expression { return params.MAC } } + steps { + dir('build') { + unstash 'mac_installer' + unstash 'mac_installer_test' + } - util.publishToNexus( - groupId: "app", - artifactId: "desktop-linux-test", - version: "${VERSION}", - assetFilePath: "${WORKSPACE}/build/desktop-test/tutanota-desktop-test-linux.AppImage", - fileExtension: 'AppImage' - ) - util.publishToNexus( - groupId: "app", - artifactId: "desktop-win-test", - version: "${VERSION}", - assetFilePath: "${WORKSPACE}/build/desktop-test/tutanota-desktop-test-win.exe", - fileExtension: 'exe' - ) - util.publishToNexus( - groupId: "app", - artifactId: "desktop-mac-test", - version: "${VERSION}", - assetFilePath: "${WORKSPACE}/build/desktop-test/tutanota-desktop-test-mac.dmg", - fileExtension: 'dmg' - ) - util.publishToNexus( - groupId: "app", - artifactId: "desktop-linux", - version: "${VERSION}", - assetFilePath: "${WORKSPACE}/build/desktop/tutanota-desktop-linux.AppImage", - fileExtension: 'AppImage' - ) - util.publishToNexus( - groupId: "app", - artifactId: "desktop-win", - version: "${VERSION}", - assetFilePath: "${WORKSPACE}/build/desktop/tutanota-desktop-win.exe", - fileExtension: 'exe' - ) - util.publishToNexus( - groupId: "app", - artifactId: "desktop-mac", - version: "${VERSION}", - assetFilePath: "${WORKSPACE}/build/desktop/tutanota-desktop-mac.dmg", - fileExtension: 'dmg' - ) - } // script upload to nexus - - } // steps - } // stage build deb & publish + withCredentials([string(credentialsId: 'HSM_USER_PIN', variable: 'PW')]) { + sh '''export HSM_USER_PIN=${PW}; node buildSrc/signDesktopClients.js''' + } + + uploadMacArtifacts() + } + } // mac + stage('Linux') { + when { expression { return params.LINUX } } + steps { + dir('build') { + unstash 'linux_installer' + unstash 'linux_installer_test' + } + + withCredentials([string(credentialsId: 'HSM_USER_PIN', variable: 'PW')]) { + sh '''export HSM_USER_PIN=${PW}; node buildSrc/signDesktopClients.js''' + } + + uploadLinuxArtifacts() + } + } // linux + } // parallel + } // stage sign and upload + } // stages + } // stage sign clients and upload to Nexus } // stages } // pipeline -void initBuildArea() { +def uploadWindowsArtifacts() { + script { + def stagingFiles = [ + ["build/desktop-test/tutanota-desktop-test-win.exe", "exe"], + ["build/desktop-test/tutanota-desktop-test-win.exe.blockmap", "exe.blockmap"], + ["build/desktop-test/win-sig.bin", "sig.bin"], + ["build/desktop-test/latest.yml", "latest.yml"], + ] + def prodFiles = [ + ["build/desktop/tutanota-desktop-win.exe", "exe"], + ["build/desktop/tutanota-desktop-win.exe.blockmap", "exe.blockmap"], + ["build/desktop/win-sig.bin", "sig.bin"], + ["build/desktop/latest.yml", "latest.yml"], + ] + + uploadFilesToNexus("desktop-win-test", stagingFiles) + uploadFilesToNexus("desktop-win", prodFiles) + } +} + +def uploadMacArtifacts() { + script { + def stagingFiles = [ + ["build/desktop-test/tutanota-desktop-test-mac.dmg", "dmg"], + ["build/desktop-test/tutanota-desktop-test-mac.dmg.blockmap", "dmg.blockmap"], + ["build/desktop-test/tutanota-desktop-test-mac.zip", "zip"], + ["build/desktop-test/tutanota-desktop-test-mac.zip.blockmap", "zip.blockmap"], + ["build/desktop-test/mac-sig-dmg.bin", "sig.dmg.bin"], + ["build/desktop-test/mac-sig-zip.bin", "sig.zip.bin"], + ["build/desktop-test/latest-mac.yml", "latest.yml"], + ] + def prodFiles = [ + ["build/desktop/tutanota-desktop-mac.dmg", "dmg"], + ["build/desktop/tutanota-desktop-mac.dmg.blockmap", "dmg.blockmap"], + ["build/desktop/tutanota-desktop-mac.zip", "zip"], + ["build/desktop/tutanota-desktop-mac.zip.blockmap", "zip.blockmap"], + ["build/desktop/mac-sig-dmg.bin", "sig.dmg.bin"], + ["build/desktop/mac-sig-zip.bin", "sig.zip.bin"], + ["build/desktop/latest-mac.yml", "latest.yml"], + ] + + uploadFilesToNexus("desktop-mac-test", stagingFiles) + uploadFilesToNexus("desktop-mac", prodFiles) + } +} + +def uploadLinuxArtifacts() { + script { + def stagingFiles = [ + ["build/desktop-test/tutanota-desktop-test-linux.AppImage", "AppImage"], + ["build/desktop-test/linux-sig.bin", "sig.bin"], + ["build/desktop-test/latest-linux.yml", "latest.yml"], + ] + def prodFiles = [ + ["build/desktop/tutanota-desktop-linux.AppImage", "AppImage"], + ["build/desktop/linux-sig.bin", "sig.bin"], + ["build/desktop/latest-linux.yml", "latest.yml"], + ] + + uploadFilesToNexus("desktop-linux-test", stagingFiles) + uploadFilesToNexus("desktop-linux", prodFiles) + } +} + +def uploadFilesToNexus(String artifactId, String[][] filesPathAndExt) { + def util = load "ci/jenkins-lib/util.groovy" + + for (String[] file in filesPathAndExt) { + if (!fileExists(file[0])) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${file[0]}") + } + +// util.publishToNexus( +// groupId: "app", +// artifactId: artifactId, +// version: "${VERSION}", +// assetFilePath: "${WORKSPACE}/${file[0]}", +// fileExtension: file[1] +// ) + } +} + +def initBuildArea() { sh 'node -v' sh 'npm -v' sh 'npm ci' diff --git a/ci/Ios.Jenkinsfile b/ci/Ios.Jenkinsfile index 3f3ec3c1a47d..c8185de6f951 100644 --- a/ci/Ios.Jenkinsfile +++ b/ci/Ios.Jenkinsfile @@ -2,7 +2,6 @@ pipeline { environment { NODE_MAC_PATH = "/usr/local/opt/node@20/bin/" VERSION = sh(returnStdout: true, script: "${env.NODE_PATH}/node -p -e \"require('./package.json').version\" | tr -d \"\n\"") - RELEASE_NOTES_PATH = "app-ios/fastlane/metadata/default/release_notes.txt" } agent { @@ -13,8 +12,8 @@ pipeline { booleanParam( name: 'RELEASE', defaultValue: false, - description: "Build testing and production version, and upload them to nexus/testflight/appstore. " + - "The production version will need to be released manually from appstoreconnect.apple.com." + description: "Upload staging/prod to Nexus and send staging version to testflight. " + + "The production version must be sent to appstore using the publish job" ) booleanParam( name: 'PROD', @@ -24,11 +23,6 @@ pipeline { name: 'STAGING', defaultValue: true ) - persistentText( - name: "releaseNotes", - defaultValue: "", - description: "release notes for this build" - ) } stages { @@ -60,7 +54,7 @@ pipeline { } } - stage("Build and upload to Apple") { + stage("Build") { environment { PATH="${env.NODE_MAC_PATH}:${env.PATH}:${env.HOME}/emsdk:${env.HOME}/emsdk/upstream/emscripten:${env.HOME}/emsdk/upstream/bin" MATCH_GIT_URL = "git@gitlab:/tuta/apple-certificates.git" @@ -68,7 +62,7 @@ pipeline { LANG = "en_US.UTF-8" } agent { - label 'mac-intel' + label 'mac' } stages { stage('Staging') { @@ -82,9 +76,10 @@ pipeline { generateXCodeProjects() util.runFastlane("de.tutao.tutanota.test", "adhoc_staging") if (params.RELEASE) { - util.runFastlane("de.tutao.tutanota.test", "testflight_staging") + util.runFastlane("de.tutao.tutanota.test", "build_mail_staging") + stash includes: "app-ios/releases/tutanota-${VERSION}-test.ipa", name: 'ipa-staging' } - stash includes: "app-ios/releases/tutanota-${VERSION}-adhoc-test.ipa", name: 'ipa-testing' + stash includes: "app-ios/releases/tutanota-${VERSION}-adhoc-test.ipa", name: 'ipa-adhoc-staging' } } } @@ -99,10 +94,10 @@ pipeline { generateXCodeProjects() util.runFastlane("de.tutao.tutanota", "adhoc_prod") if (params.RELEASE) { - writeReleaseNotesForAppStore() - util.runFastlane("de.tutao.tutanota", "appstore_prod submit:true") + util.runFastlane("de.tutao.tutanota", "build_mail_prod") + stash includes: "app-ios/releases/tutanota-${VERSION}.ipa", name: 'ipa-production' } - stash includes: "app-ios/releases/tutanota-${VERSION}-adhoc.ipa", name: 'ipa-production' + stash includes: "app-ios/releases/tutanota-${VERSION}-adhoc.ipa", name: 'ipa-adhoc-production' } } } @@ -122,48 +117,20 @@ pipeline { steps { script { if (params.STAGING) { - unstash 'ipa-testing' - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'There was an error when uploading to Nexus') { - publishToNexus("ios-test", "tutanota-${VERSION}-adhoc-test.ipa") - } + unstash 'ipa-adhoc-staging' + unstash 'ipa-staging' + + publishToNexus("ios-test", "tutanota-${VERSION}-adhoc-test.ipa") + publishToNexus("ios-test", "tutanota-${VERSION}-test.ipa") } if (params.PROD) { + unstash 'ipa-adhoc-production' unstash 'ipa-production' - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'There was an error when uploading to Nexus') { - publishToNexus("ios", "tutanota-${VERSION}-adhoc.ipa") - } - } - } - } - } - - stage('Tag and create github release page') { - environment { - PATH = "${env.NODE_PATH}:${env.PATH}" - } - when { - expression { return params.RELEASE } - } - agent { - label 'linux' - } - steps { - script { - - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to create github release page for ios') { - def tag = "tutanota-ios-release-${VERSION}" - // need to run npm ci to install dependencies of releaseNotes.js - sh "npm ci" - writeFile file: "notes.txt", text: params.releaseNotes - withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { - sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (iOS)' \ - --tag 'tutanota-ios-release-${VERSION}' \ - --notes notes.txt""" - } // withCredentials - sh "rm notes.txt" - } // catchError + publishToNexus("ios", "tutanota-${VERSION}-adhoc.ipa") + publishToNexus("ios", "tutanota-${VERSION}.ipa") + } } } } @@ -174,7 +141,7 @@ void stubClientDirectory() { script { sh "pwd" sh "echo $PATH" - sh "mkdir build-calendar-app" + sh "mkdir build-calendar-app" sh "mkdir build" } } @@ -209,26 +176,6 @@ void generateCalendarProject() { generateXCodeProject("app-ios", "calendar-project") } - -void writeReleaseNotesForAppStore() { - script { - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to create github release notes for ios') { - // need to run npm ci to install dependencies of releaseNotes.js - sh "npm ci" - writeFile file: "notes.txt", text: params.releaseNotes - withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { - sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (iOS)' \ - --tag 'tutanota-ios-release-${VERSION}'\ - --notes notes.txt \ - --toFile ${RELEASE_NOTES_PATH}""" - } - sh "rm notes.txt" - } - } - - sh "echo Created release notes for fastlane ${RELEASE_NOTES_PATH}" -} - void publishToNexus(String artifactId, String ipaFileName) { def util = load "ci/jenkins-lib/util.groovy" util.publishToNexus(groupId: "app", diff --git a/ci/Publish-CalendarMobileArtifacts.Jenkinsfile b/ci/Publish-CalendarMobileArtifacts.Jenkinsfile index 99d2c2d33ca7..864bfab981c0 100644 --- a/ci/Publish-CalendarMobileArtifacts.Jenkinsfile +++ b/ci/Publish-CalendarMobileArtifacts.Jenkinsfile @@ -128,12 +128,12 @@ pipeline { } steps { script { - catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to upload android app to GitHub') { + catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to upload iOS app to GitHub') { writeReleaseNotes("ios", "iOS", "${env.VERSION}", "") } } // script } // steps - } // stage Android App + } // stage iOS App } } stage("Publishing Artifacts to Stores") { diff --git a/ci/Publish-MailDesktopArtifacts.Jenkinsfile b/ci/Publish-MailDesktopArtifacts.Jenkinsfile new file mode 100644 index 000000000000..70fba89ba01d --- /dev/null +++ b/ci/Publish-MailDesktopArtifacts.Jenkinsfile @@ -0,0 +1,177 @@ +pipeline { + environment { + // on m1 macs, this is a symlink that must be updated. see wiki. + VERSION = sh(returnStdout: true, script: "${env.NODE_PATH}/node -p -e \"require('./package.json').version\" | tr -d \"\n\"") + TMPDIR ='/tmp' + LINUX_IMAGE_PATH = 'build/desktop/tutanota-desktop-linux.AppImage' + } + + parameters { + booleanParam( + name: 'DEB', + defaultValue: false, + description: "build deb package" + ) + booleanParam( + name: 'PUBLISH_NOTES', + defaultValue: false, + description: "publish release notes draft" + ) + persistentText( + name: 'releaseNotes', + defaultValue: '', + description: "release notes for this build" + ) + } + + agent { + label 'master' + } + + stages { + stage('Check Github') { + steps { + script { + def util = load "ci/jenkins-lib/util.groovy" + util.checkGithub() + } + } + } // check github + stage('Preparation for build deb and publish notes') { + agent { + label 'master' + } + steps { + script { + def devicePath = sh(script: 'lsusb | grep Nitro | sed -nr \'s|Bus (.*) Device ([^:]*):.*|/dev/bus/usb/\\1/\\2|p\'', returnStdout: true).trim() + env.DEVICE_PATH = devicePath + } + } + } // preparation for build deb and publish notes + stage ('Build deb and publish release notes draft') { + when { expression { return params.PUBLISH_NOTES || params.DEB } } + agent { + dockerfile { + filename 'linux-build.dockerfile' + label 'master' + dir 'ci/containers' + additionalBuildArgs '--format docker' + args "--network host -v /run:/run:rw,z -v /opt/repository:/opt/repository:rw,z --device=${env.DEVICE_PATH}" + } // docker + } // agent + stages { + stage('Publish release notes draft') { + when { expression { return params.PUBLISH_NOTES } } + steps { + script { + def desktopLinux = env.LINUX_IMAGE_PATH + def desktopWin = "build/desktop/tutanota-desktop-win.exe" + def desktopMac = "build/desktop/tutanota-desktop-mac.dmg" + + def util = load "ci/jenkins-lib/util.groovy" + + util.downloadFromNexus( groupId: "app", + artifactId: "desktop-win", + version: "${VERSION}", + outFile: "${WORKSPACE}/${desktopWin}", + fileExtension: 'exe') + if (!fileExists("${desktopWin}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${desktopWin}") + } + + util.downloadFromNexus( groupId: "app", + artifactId: "desktop-mac", + version: "${VERSION}", + outFile: "${WORKSPACE}/${desktopMac}", + fileExtension: 'dmg') + if (!fileExists("${desktopMac}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${desktopMac}") + } + + util.downloadFromNexus( groupId: "app", + artifactId: "desktop-linux", + version: "${VERSION}", + outFile: "${WORKSPACE}/${desktopLinux}", + fileExtension: 'AppImage') + if (!fileExists("${desktopLinux}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${desktopLinux}") + } + + writeFile file: "notes.txt", text: params.releaseNotes + catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to create github release page for desktop') { + withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { + sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (Desktop)' \ + --tag 'tutanota-desktop-release-${VERSION}' \ + --uploadFile '${WORKSPACE}/${desktopLinux}' \ + --uploadFile '${WORKSPACE}/${desktopWin}' \ + --uploadFile '${WORKSPACE}/${desktopMac}' \ + --notes notes.txt""" + } // withCredentials + } // catchError + sh "rm notes.txt" + + stash includes: desktopLinux, name: 'linux_image' + } // script release draft + } // steps + } // publish + stage('Build webapp') { + when { expression { return params.DEB } } + steps { + sh 'npm ci' + sh 'npm run build-packages' + sh 'node webapp.js release' + + // excluding web-specific and mobile specific parts which we don't need in desktop + stash includes: 'build/**', excludes: '**/braintree.html, **/index.html, **/app.html, **/desktop.html, **/index-index.js, **/index-app.js, **/index-desktop.js, **/sw.js', name: 'web_base' + } + } + stage('Build deb') { + when { expression { return params.DEB } } + steps { + script { + def desktopLinux = env.LINUX_IMAGE_PATH + def desktopLinuxTest = "build/desktop-test/tutanota-desktop-test-linux.AppImage" + + def util = load "ci/jenkins-lib/util.groovy" + + if (params.PUBLISH_NOTES) { + unstash 'linux_image' + } else { + util.downloadFromNexus( groupId: "app", + artifactId: "desktop-linux", + version: "${VERSION}", + outFile: "${WORKSPACE}/${desktopLinux}", + fileExtension: 'AppImage') + } + if (!fileExists("${desktopLinux}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${desktopLinux}") + } + + util.downloadFromNexus( groupId: "app", + artifactId: "desktop-linux-test", + version: "${VERSION}", + outFile: "${WORKSPACE}/${desktopLinuxTest}", + fileExtension: 'AppImage') + if (!fileExists("${desktopLinuxTest}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${desktopLinuxTest}") + } + } // script build deb + + sh 'node -v' + sh 'npm -v' + sh 'npm ci' + sh 'npm run build-packages' + unstash 'web_base' + sh 'node buildSrc/publish.js desktop' + sh 'rm -rf ./build/*' + } // steps build deb + } // build deb + } // stages + } // build deb and publish notes + } // stages +} // pipeline \ No newline at end of file diff --git a/ci/Publish-MailMobileArtifacts.Jenkinsfile b/ci/Publish-MailMobileArtifacts.Jenkinsfile new file mode 100644 index 000000000000..174b242c583e --- /dev/null +++ b/ci/Publish-MailMobileArtifacts.Jenkinsfile @@ -0,0 +1,264 @@ +pipeline { + environment { + NODE_MAC_PATH = "/usr/local/opt/node@20/bin/" + PATH="${env.NODE_PATH}:${env.PATH}" + VERSION = sh(returnStdout: true, script: "${env.NODE_PATH}/node -p -e \"require('./package.json').version\" | tr -d \"\n\"") + } + + parameters { + booleanParam( + name: 'googlePlayStore', + defaultValue: false, + description: "Uploads android artifacts (apk) to Google PlayStore as a Draft on the public track." + ) + booleanParam( + name: 'appleAppStore', + defaultValue: false, + description: "Uploads iOS artifacts to Apple App Store as a Draft on the public track." + ) + string( + name: 'appVersion', + defaultValue: "", + description: 'Which version should be published' + ) + booleanParam( + name: 'androidGithub', + defaultValue: false, + description: "Uploads android artifact (apk) to GitHub and publish release notes" + ) + booleanParam( + name: 'iOSGithub', + defaultValue: false, + description: "Publish iOS release notes to github" + ) + persistentText( + name: 'androidReleaseNotes', + defaultValue: "", + description: 'Android release notes' + ) + persistentText( + name: 'iosReleaseNotes', + defaultValue: "", + description: "iOS release notes" + ) + } + + agent { + label 'linux' + } + + stages { + stage("Checking params") { + steps { + script{ + //#FIXME make sure at least something runs + if() { + currentBuild.result = 'ABORTED' + error('No artifacts were selected.') + } + } + echo "Params OKAY" + } + } + stage("Android") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-android-release-${VERSION}" + FILE_PATH = "build/app-android/tutanota-app-tutao-release-${VERSION}.apk" + } + // #FIXME + when {} + stages { + stage("Google Play Store") { + parallel { + stage("Staging") { + steps { + script { + def filePath = "build/app-android/tutanota-app-tutao-releaseTest-${VERSION}.apk" + + downloadAndroidApp("android-test", filePath) + + // This doesn't publish to the main app on play store, + // instead it gets published to the hidden "tutanota-test" app + // this happens because the AppId is set to de.tutao.tutanota.test by the android build + // and play store knows which app to publish just based on the id + androidApkUpload( + googleCredentialsId: 'android-app-publisher-credentials', + apkFilesPattern: filePath, + trackName: 'internal', + rolloutPercentage: '100%', + recentChangeList: [ + [ + language: "en-US", + text : "see: ${GITHUB_RELEASE_PAGE}" + ] + ] + ) // androidApkUpload + } // script + } // steps + } // stage Testing + stage("Production") { + steps { + script { + downloadAndroidApp("android", FILE_PATH) + + androidApkUpload( + googleCredentialsId: 'android-app-publisher-credentials', + apkFilesPattern: "${FILE_PATH}", + trackName: 'production', + // Don't publish the app to users directly + // It will require manual intervention at play.google.com/console + rolloutPercentage: '0%', + recentChangeList: [ + [ + language: "en-US", + text : "see: ${GITHUB_RELEASE_PAGE}" + ] + ] + ) + } // script + } // steps + } // stage Production + } // parallel + } // stage Google Play Store + stage("Github release notes") { + steps { + script { + downloadAndroidApp("android", FILE_PATH) + + sh 'npm ci' + + writeFile file: "notes.txt", text: params.androidReleaseNotes + withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { + sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (Android)' \ + --tag 'tutanota-android-release-${VERSION}' \ + --uploadFile '${WORKSPACE}/${FILE_PATH}' \ + --notes notes.txt""" + } // withCredentials + sh "rm notes.txt" + } // script + } // steps + } + } // stages Android + } // stage Android + stage("iOS") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-ios-release-${VERSION}" + FILE_PATH_STAGING = "app-ios/releases/tutanota-${VERSION}-test.ipa" + FILE_PATH_PROD = "app-ios/releases/tutanota-${VERSION}.ipa" + } + stages { + stage("Download artifacts") { + agent { + label 'linux' + } + steps { + downloadIOSApp("ios-test", FILE_PATH_STAGING) + downloadIOSApp("ios", FILE_PATH_PROD) + + stash includes: FILE_PATH_STAGING, name: 'ipa-staging' + stash includes: FILE_PATH_PROD, name: 'ipa-prod' + } + } + stage("Apple App Store") { + environment { + PATH="${env.NODE_MAC_PATH}:${env.PATH}:${env.HOME}/emsdk:${env.HOME}/emsdk/upstream/emscripten:${env.HOME}/emsdk/upstream/bin" + MATCH_GIT_URL = "git@gitlab:/tuta/apple-certificates.git" + LC_ALL = "en_US.UTF-8" + LANG = "en_US.UTF-8" + } + agent { + label 'mac' + } + stages { + parallel { + stage("Staging") { + steps { + unstash "ipa-staging" + + script { + def util = load "ci/jenkins-lib/util.groovy" + util.runFastlane("de.tutao.tutanota.test", "publish_mail_staging file:${WORKSPACE}/${FILE_PATH_STAGING}") + + } + } + } + stage("Production") { + steps { + writeReleaseNotesForAppStore() + unstash "ipa-prod" + + script { + def util = load "ci/jenkins-lib/util.groovy" + util.runFastlane("de.tutao.tutanota", "publish_mail_prod file:${WORKSPACE}/${FILE_PATH_PROD}") + } + } + } + } // parallel + } // stages + } // stage Apple App Store + stage("GitHub release notes") { + steps { + script { + sh 'npm ci' + + writeFile file: "notes.txt", text: params.iosReleaseNotes + withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { + sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (iOS)' \ + --tag 'tutanota-ios-release-${VERSION}' \ + --notes notes.txt""" + } // withCredentials + sh "rm notes.txt" + } // script + } // steps + } // stage GitHub release notes + } + } // stage iOS + } // stages +} // pipeline + +def downloadAndroidApp(String artifactId, String filePath) { + def util = load "ci/jenkins-lib/util.groovy" + + util.downloadFromNexus( groupId: "app", + artifactId: artifactId, + version: "${VERSION}", + outFile: "${WORKSPACE}/${filePath}", + fileExtension: 'apk') + + if (!fileExists("${filePath}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${filePath}") + } + echo "File ${filePath} found!" +} + +def downloadIOSApp(String artifactId, String filePath) { + def util = load "ci/jenkins-lib/util.groovy" + + util.downloadFromNexus(groupId: "app", + artifactId: "${artifactId}", + version: "${VERSION}", + assetFilePath: "${WORKSPACE}/${filePath}", + fileExtension: "ipa" + ) + + if (!fileExists("${filePath}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${filePath}") + } + echo "File ${filePath} found!" +} + +def writeReleaseNotesForAppStore() { + // need to run npm ci to install dependencies of releaseNotes.js + sh "npm ci" + writeFile file: "notes.txt", text: params.iosReleaseNotes + sh """node buildSrc/createReleaseDraft.js --name '${VERSION} (iOS)' \ + --tag 'tutanota-ios-release-${VERSION}'\ + --notes notes.txt \ + --toFile ${RELEASE_NOTES_PATH}""" + sh "rm notes.txt" + sh "echo Created release notes for fastlane ${RELEASE_NOTES_PATH}" +} \ No newline at end of file diff --git a/ci/Release.Jenkinsfile b/ci/Release.Jenkinsfile index 391cb157bb80..cc7964eb6e85 100644 --- a/ci/Release.Jenkinsfile +++ b/ci/Release.Jenkinsfile @@ -45,9 +45,24 @@ pipeline { description: "Build the android app" ) booleanParam( - name: 'desktop', + name: 'windows', defaultValue: true, - description: "Build the desktop app" + description: "Build the windows app" + ) + booleanParam( + name: 'mac', + defaultValue: true, + description: "Build the mac app" + ) + booleanParam( + name: 'linux', + defaultValue: true, + description: "Build the linux app" + ) + booleanParam( + name: 'deb', + defaultValue: false, + description: "Build the deb package" ) } @@ -64,11 +79,9 @@ pipeline { script { // create release notes def version = sh(returnStdout: true, script: "${NODE_PATH}/node -p -e \"require('./package.json').version\" | tr -d \"\n\"") def web = params.web ? pregenerateReleaseNotes("web") : null - def android = params.android ? pregenerateReleaseNotes("android") : null - def ios = params.ios ? pregenerateReleaseNotes("ios") : null def desktop = params.desktop ? pregenerateReleaseNotes("desktop") : null - releaseNotes = reviewReleaseNotes(web, android, desktop, ios, version) + releaseNotes = reviewReleaseNotes(web, desktop, version) echo("${releaseNotes}") } // script release notes } // steps @@ -94,48 +107,81 @@ pipeline { } } stage("Desktop Client") { - when { expression { return params.desktop } } - steps { - script { - build job: 'tutanota-3-desktop', parameters: params.generateReleaseNotes ? [ - booleanParam(name: "RELEASE", value: !params.dryRun), - text(name: "releaseNotes", value: releaseNotes.desktop), - ] : [ - booleanParam(name: "RELEASE", value: !params.dryRun), - ] - } // script - } // steps - } // stage desktop client - stage("iOS Client") { - when { expression { return params.ios } } - steps { - script { - build job: 'tutanota-3-ios', parameters: params.generateReleaseNotes ? [ - booleanParam(name: "RELEASE", value: !params.dryRun), - text(name: "releaseNotes", value: releaseNotes.ios), - booleanParam(name: "STAGING", value: true), - booleanParam(name: "PROD", value: true), - ] : [ - booleanParam(name: "RELEASE", value: !params.dryRun), - booleanParam(name: "STAGING", value: true), - booleanParam(name: "PROD", value: true), - ] - } // script - } // steps - } // stage desktop client - stage("Android Client") { - when { expression { return params.android } } - steps { - script { - build job: 'tutanota-3-android', parameters: params.generateReleaseNotes ? [ - booleanParam(name: "RELEASE", value: !params.dryRun), - text(name: "releaseNotes", value: releaseNotes.android), - ] : [ - booleanParam(name: "RELEASE", value: !params.dryRun), - ] - } // script - } // steps + stages { + stage("Build and upload to Nexus") { + when { expression { return params.windows || params.mac || params.linux } } + steps { + script { + // FIXME: create temp job for testing + build job: 'temp-tutanota-3-desktop', parameters: [ + booleanParam(name: "UPLOAD", value: !params.dryRun), + booleanParam(name: "WINDOWS", value: params.windows), + booleanParam(name: "MAC", value: params.mac), + booleanParam(name: "LINUX", value: params.linux), + ] + } // script + } // steps + } + stage("Build deb and publish notes") { + when { expression { return !params.dryRun && (params.generateReleaseNotes || params.deb) } } + steps { + script { + // FIXME: create job + build job: 'tutanota-3-desktop-publish', parameters: params.generateReleaseNotes ? [ + booleanParam(name: "PUBLISH_NOTES", value: true), + text(name: "releaseNotes", value: releaseNotes.desktop), + booleanParam(name: "DEB", value: params.deb), + ] : [ + booleanParam(name: "DEB", value: params.deb), + ] + } // script + } // steps + } + } } // stage desktop client + stage("Mobile Client") { + stages { + stage("iOS Client") { + when { expression { return params.ios } } + steps { + script { + // FIXME: create temp job for testing + build job: 'temp-tutanota-3-ios', parameters: [ + booleanParam(name: "RELEASE", value: !params.dryRun), + booleanParam(name: "STAGING", value: true), + booleanParam(name: "PROD", value: true), + ] + } // script + } // steps + } + stage("Android Client") { + when { expression { return params.android } } + steps { + script { + // FIXME: create temp job for testing + build job: 'temp-tutanota-3-android', parameters: [ + booleanParam(name: "RELEASE", value: !params.dryRun), + ] + } // script + } // steps + } + stage("Publish mobile artifacts") { + when { expression { return !params.dryRun || params.generateReleaseNotes } } + steps { + script { + // FIXME: create job + build job: 'tutanota-3-mobile-publish', parameters: [ + text(name: "appVersion", value: params.milestone), + booleanParam(name: "generateReleaseNotes", value: params.generateReleaseNotes), + booleanParam(name: "github", value: params.generateReleaseNotes), + booleanParam(name: "googlePlayStore", value: !params.dryRun), + booleanParam(name: "appleAppStore", value: !params.dryRun), + ] + } // script + } // steps + } + } // stages + } // stage mobile client } // parallel clients } // stage other clients } // stages @@ -153,13 +199,11 @@ def pregenerateReleaseNotes(platform) { /** all parameters are nullable strings. */ -def reviewReleaseNotes(web, android, desktop, ios, version) { +def reviewReleaseNotes(web, desktop, version) { // only display input fields for the clients we're actually building. def parameters = [ web ? text(defaultValue: web, description: "Web App:", name: "web") : null, - android ? text(defaultValue: android, description: "Android App:", name: "android") : null, desktop ? text(defaultValue: desktop, description: 'Desktop Client:', name: 'desktop') : null, - ios ? text(defaultValue: ios, description: 'Ios App:', name: 'ios') : null, booleanParam(defaultValue: true, description: "dummy param so we always get a dict back", name: "dummy"), ].findAll { it != null } // Get the input diff --git a/ci/jenkins-lib/util.groovy b/ci/jenkins-lib/util.groovy index 7232a6c49bba..88bce0f02ff1 100644 --- a/ci/jenkins-lib/util.groovy +++ b/ci/jenkins-lib/util.groovy @@ -26,12 +26,13 @@ def downloadFromNexus(Map params) { def checkGithub() { // this fails if the public repository master's tip is not in our master. // we may have more commits, though. - sh ''' - # commit hash of the public repositories master - gh=$(git ls-remote git@github.com:tutao/tutanota.git refs/heads/master | awk '{print $1}') - # exit with 0 if $gh is an ancestor of the current HEAD, 1 otherwise. - git merge-base --is-ancestor $gh HEAD - ''' +// FIXME +// sh ''' +// # commit hash of the public repositories master +// gh=$(git ls-remote git@github.com:tutao/tutanota.git refs/heads/master | awk '{print $1}') +// # exit with 0 if $gh is an ancestor of the current HEAD, 1 otherwise. +// git merge-base --is-ancestor $gh HEAD +// ''' }