diff --git a/ci/Android.Jenkinsfile b/ci/Android.Jenkinsfile index 49cf94df7125..ce193f668939 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 { @@ -23,11 +22,6 @@ pipeline { "Uploads both to Nexus and creates a new release on google play, " + "which must be manually published from play.google.com/console" ) - persistentText( - name: "releaseNotes", - defaultValue: "", - description: "release notes for this build" - ) } stages { @@ -124,26 +118,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 +137,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/Ios.Jenkinsfile b/ci/Ios.Jenkinsfile index 3f3ec3c1a47d..10d42dbe4c48 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 { @@ -24,11 +23,6 @@ pipeline { name: 'STAGING', defaultValue: true ) - persistentText( - name: "releaseNotes", - defaultValue: "", - description: "release notes for this build" - ) } stages { @@ -99,7 +93,6 @@ pipeline { generateXCodeProjects() util.runFastlane("de.tutao.tutanota", "adhoc_prod") if (params.RELEASE) { - writeReleaseNotesForAppStore() util.runFastlane("de.tutao.tutanota", "appstore_prod submit:true") } stash includes: "app-ios/releases/tutanota-${VERSION}-adhoc.ipa", name: 'ipa-production' @@ -137,36 +130,6 @@ pipeline { } } } - - 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 - } - } - } } } @@ -209,26 +172,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-MailMobileArtifacts.Jenkinsfile b/ci/Publish-MailMobileArtifacts.Jenkinsfile new file mode 100644 index 000000000000..89f58bd016c2 --- /dev/null +++ b/ci/Publish-MailMobileArtifacts.Jenkinsfile @@ -0,0 +1,315 @@ +import groovy.transform.Field +@Field def releaseNotes + +pipeline { + environment { + PATH="${env.NODE_PATH}:${env.PATH}" + VERSION = sh(returnStdout: true, script: "${env.NODE_PATH}/node -p -e \"require('./package.json').version\" | tr -d \"\n\"") + IOS_RELEASE_NOTES_PATH = "app-ios/fastlane/metadata/default/release_notes.txt" + } + + 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." + ) + booleanParam( + name: 'github', + defaultValue: false, + description: "Uploads android artifact (apk) to GitHub and publish release notes." + ) + string( + name: 'appVersion', + defaultValue: "", + description: 'Which version should be published.' + ) + booleanParam( + name: "generateReleaseNotes", + defaultValue: true, + description: "Generate Release notes for this build." + ) + } + + agent { + label 'linux' + } + + stages { + stage("Checking params") { + steps { + script{ + if(!params.googlePlayStore && !params.appleAppStore && !params.github) { + currentBuild.result = 'ABORTED' + error('No artifacts were selected.') + } + } + echo "Params OKAY" + } + } + stage("Prepare Release Notes") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + } + when { + expression { + params.generateReleaseNotes && (params.googlePlayStore || params.appleAppStore || params.github) + } + } + steps { + sh "npm ci" + script { // create release notes + def android = params.googlePlayStore || params.github ? pregenerateReleaseNotes("android", env.VERSION) : null + def ios = params.appleAppStore || params.github ? pregenerateReleaseNotes("ios", env.VERSION) : null + + // Assigns the dict returned by reviewReleaseNotes with the notes for each platform to the global var releaseNotes + releaseNotes = reviewReleaseNotes(android, ios, env.VERSION) + + if (params.appleAppStore || params.github) { + env.IOS_RELEASE_NOTES = releaseNotes.ios + echo releaseNotes.ios + } + + if (params.googlePlayStore || params.github) { + env.ANDROID_RELEASE_NOTES = releaseNotes.android + echo releaseNotes.android + } + } + } // steps + } // stage Prepare Release Notes + stage("GitHub Release") { + stages { + stage("GitHub Android Tag") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + FILE_PATH = "build/app-android/tutanota-app-tutao-release-${VERSION}.apk" + GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-android-release-${VERSION}" + } + when { + expression { + params.github && releaseNotes.android.trim() + } + } + steps { + script { + def util = load "ci/jenkins-lib/util.groovy" + util.downloadFromNexus( groupId: "app", + artifactId: "android", + version: "${env.VERSION}", + outFile: "${env.WORKSPACE}/${env.FILE_PATH}", + fileExtension: 'apk') + + if (!fileExists("${env.FILE_PATH}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${env.FILE_PATH}") + } + echo "File ${env.FILE_PATH} found!" + + catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to upload android app to GitHub') { + writeReleaseNotes("android", "Android", "${env.VERSION}", "${env.WORKSPACE}/${env.FILE_PATH}") + } + } // script + } // steps + } // stage Android App + stage("GitHub iOS Tag") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-ios-release-${VERSION}" + } + when { + expression { + params.github && releaseNotes.ios + } + } + steps { + script { + catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to upload iOS app to GitHub') { + writeReleaseNotes("ios", "iOS", "${env.VERSION}", "") + } + } // script + } // steps + } // stage iOS App + } + } + stage("Publishing Artifacts to Stores") { + parallel { + stage("Android App") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + FILE_PATH = "build/app-android/tutanota-app-tutao-release-${VERSION}.apk" + GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-android-release-${VERSION}" + } + when { + expression { + params.googlePlayStore + } + } + steps { + script { + def util = load "ci/jenkins-lib/util.groovy" + util.downloadFromNexus( groupId: "app", + artifactId: "android", + version: "${env.VERSION}", + outFile: "${env.WORKSPACE}/${env.FILE_PATH}", + fileExtension: 'apk') + if (!fileExists("${env.FILE_PATH}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${env.FILE_PATH}") + } + echo "File ${env.FILE_PATH} found!" + + catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: 'Failed to upload android test app to Play Store') { + androidApkUpload( + googleCredentialsId: 'android-app-publisher-credentials', + apkFilesPattern: "${env.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: ${env.GITHUB_RELEASE_PAGE}" + ] + ] + ) + } + } // script + } // steps + } // stage Android App + stage("iOS App") { + environment { + VERSION = "${params.appVersion.trim() ?: env.VERSION}" + FILE_PATH = "app-ios/releases/tutanota-${VERSION}-adhoc.ipa" + GITHUB_RELEASE_PAGE = "https://github.com/tutao/tutanota/releases/tag/tutanota-ios-release-${VERSION}" + } + stages { + stage("Download artifact") { + when { + expression { + params.appleAppStore || params.appleTestflight + } + } + steps { + script { + def util = load "ci/jenkins-lib/util.groovy" + util.downloadFromNexus(groupId: "app", + artifactId: "ios", + version: "${env.VERSION}", + outFile: "${env.WORKSPACE}/${env.FILE_PATH}", + fileExtension: "ipa") + + if (!fileExists("${env.FILE_PATH}")) { + currentBuild.result = 'ABORTED' + error("Unable to find file ${env.FILE_PATH}") + } + echo "File ${env.FILE_PATH} found!" + stash includes: "${env.FILE_PATH}", name: 'ipa-production' + } + } + } + stage("Publish to AppStore") { + environment { + MATCH_GIT_URL = "git@gitlab:/tuta/apple-certificates.git" + } + when { + expression { + params.appleAppStore + } + } + agent { + label 'mac-intel' + } + + steps { + script { + def util = load "ci/jenkins-lib/util.groovy" + dir("${env.WORKSPACE}") { + unstash 'ipa-production' + } + generateXCodeProject("app-ios", "mail-project") + util.runFastlane("de.tutao.tutanota", "adhoc_prod") + } + } + } + } + } // stage iOS App + } // parallel apps + } // stage Publishing Artifacts + } // stages +} // pipeline + + +// Runs xcodegen on `projectPath`, a directory containing a `project.yml` +void generateXCodeProject(String projectPath, String spec) { + // xcodegen ignores its --project and --project-roots flags + // so we need to change the directory manually + script { + sh "(cd ${projectPath}; xcodegen generate --spec ${spec}.yml)" + } +} + +// Runs xcodegen on all of our project specs +void generateXCodeProjects() { + generateXCodeProject("app-ios", "mail-project") + generateXCodeProject("tuta-sdk/ios", "project") +} + +/** +platform must be one of the strings "ios", "android" +*/ +def pregenerateReleaseNotes(platform, version) { + return sh(returnStdout: true, script: """node buildSrc/releaseNotes.js --platform ${platform} --milestone ${version} """) +} + +/** + all parameters are nullable strings. +*/ +def reviewReleaseNotes(android, ios, version) { + // only display input fields for the clients we're actually building. + def parameters = [ + android ? text(defaultValue: android, description: "Android release notes built from Github Milestone", name: "android") : null, + ios ? text(defaultValue: ios, description: 'Ios release notes built from Github Milestone', name: 'ios') : null, + // If the dummy field is removed, when there is only an option a string will be returned instead(we dont want that) + booleanParam(defaultValue: true, description: "dummy param so we always get a dict back", name: "dummy"), + ].findAll { it != null } + // Get the input + // https://www.jenkins.io/doc/pipeline/steps/pipeline-input-step/ + return input(id: 'releaseNotesInput', message: 'Release Notes', parameters: parameters) +} + +/** +platform must be one of the strings "ios", "android" +filePath can be null +*/ +def writeReleaseNotes(String platform, String displayName, String version, String filePath) { + script { + catchError(stageResult: 'UNSTABLE', buildResult: 'SUCCESS', message: "Failed to create github release page for ${platform}") { + sh "npm ci" + writeFile file: "notes.txt", text: platform == "ios" ? releaseNotes.ios : releaseNotes.android + withCredentials([string(credentialsId: 'github-access-token', variable: 'GITHUB_TOKEN')]) { + def releaseDraftCommand = """node buildSrc/createReleaseDraft.js --name '[Mail] ${version} (${displayName})' \ + --tag 'tutanota-${platform}-release-${version}' \ + --notes notes.txt""" + // We don't upload iOS artifacts to GitHub + if (filePath != "" && platform == "android") { + releaseDraftCommand = "${releaseDraftCommand} --uploadFile ${filePath}" + } else if (platform == "ios") { + // Generate release notes to fastlane + sh "${releaseDraftCommand} --toFile ${IOS_RELEASE_NOTES_PATH}" + } + + sh releaseDraftCommand + } + + sh "rm notes.txt" + } + } + + sh "echo Created release notes for ${platform}" +} 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 +// ''' }