mirror of
https://github.com/tutao/tutanota.git
synced 2025-12-07 13:49:47 +00:00
Co-authored-by: das <das@tutao.de> Co-authored-by: sug <sug@tutao.de> Co-authored-by: Kinan <104761667+kibibytium@users.noreply.github.com>
559 lines
14 KiB
Groovy
559 lines
14 KiB
Groovy
import groovy.json.JsonSlurper
|
|
|
|
HashSet<String> changeset = new HashSet<String>()
|
|
String linuxWorkspace = '/opt/jenkins/jobs/bootleg-ci-merge/workspace-0'
|
|
ArrayList<String> linuxWorkspaceClones = [
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-1',
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-2',
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-3',
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-4',
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-5',
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-6',
|
|
'/opt/jenkins/jobs/bootleg-ci-merge/workspace-7'
|
|
]
|
|
String macWorkspace = '/Users/jenkins/jenkins/workspace/bootleg-ci-merge/'
|
|
|
|
pipeline {
|
|
environment {
|
|
VERSION = sh(returnStdout: true, script: "${env.NODE_PATH}/node -p -e \"require('./package.json').version\" | tr -d \"\n\"")
|
|
APK_SIGN_STORE = '/opt/android-keystore/android.jks'
|
|
PATH = "${env.NODE_PATH}:${env.PATH}:/home/jenkins/emsdk/upstream/bin/:/home/jenkins/emsdk/:/home/jenkins/emsdk/upstream/emscripten:/usr/lib/bin:/opt/homebrew/bin"
|
|
ANDROID_SDK_ROOT = "/opt/android-sdk"
|
|
ANDROID_HOME = "/opt/android-sdk"
|
|
}
|
|
|
|
agent {
|
|
label 'linux'
|
|
}
|
|
|
|
options {
|
|
// as long as tests like node/browser/android are run sequentially the timeout needs to be higher than 10 minutes
|
|
timeout(time: 15, unit: 'MINUTES')
|
|
// this prevents jenkins from running the "Check out from version control" step in every stage.
|
|
// we're running several stages in parallel on the same folder, which means git will run in parallel, which
|
|
// it can't because it places a lock file in .git
|
|
skipDefaultCheckout true
|
|
}
|
|
|
|
tools {
|
|
jdk 'jdk-21.0.2'
|
|
}
|
|
|
|
parameters {
|
|
validatingString(
|
|
// this branch will be the initial branch checked out on the job.
|
|
// this is important because if we update the jenkinsfile in a commit
|
|
// we need to run the new version, not the one from master.
|
|
name: 'SOURCE_BRANCH',
|
|
defaultValue: 'dummy-do-not-use',
|
|
description: "Branch name (no 'origin' or similar) that gets merged into TARGET_BRANCH",
|
|
regex: /^(?!dummy-do-not-use$).*$/,
|
|
failedValidationMessage: "please provide one source branch name!",
|
|
)
|
|
validatingString(
|
|
name: 'TARGET_BRANCH',
|
|
defaultValue: 'dummy-do-not-use',
|
|
description: "Branch name (no 'origin' or similar) that gets updated",
|
|
regex: /^(?!dummy-do-not-use$).*$/,
|
|
failedValidationMessage: "please provide one target branch name!",
|
|
)
|
|
booleanParam(
|
|
name: 'CLEAN_WORKSPACE',
|
|
defaultValue: false,
|
|
description: "run 'git clean -dfx' as the first step of the pipeline"
|
|
)
|
|
booleanParam(
|
|
name: 'DRY_RUN',
|
|
defaultValue: false,
|
|
description: "run the tests, but don't push to TARGET_BRANCH"
|
|
)
|
|
booleanParam(
|
|
name: 'FORCE_RUN_ALL',
|
|
defaultValue: false,
|
|
description: "run ALL the tests, even if they would normally be pruned because there are no relevant changes."
|
|
)
|
|
}
|
|
|
|
stages {
|
|
stage("repo prep") {
|
|
/**
|
|
* each physical node / container has to have a workspace prepared to do any of the checks we want to run.
|
|
*
|
|
* Most checks could run in parallel because they're read-only workloads, but jenkins really likes allocating
|
|
* new workspaces when it detects two stages running in parallel on the same workspace, even when explicitly
|
|
* told where to run the stages.
|
|
*
|
|
* we get around this by first preparing the workspace and then making symlinks for jenkins to use as
|
|
* "separate" workspaces that are already initialized.
|
|
*/
|
|
parallel {
|
|
stage("checkout linux") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspace
|
|
}
|
|
}
|
|
steps {
|
|
assertBranchWasSet("TARGET_BRANCH", params.TARGET_BRANCH)
|
|
assertBranchWasSet("SOURCE_BRANCH", params.SOURCE_BRANCH)
|
|
script {
|
|
currentBuild.displayName = "${params.DRY_RUN ? "DRY_RUN " : ""}${params.SOURCE_BRANCH} -> ${params.TARGET_BRANCH}"
|
|
}
|
|
initWorkspace(changeset, params.SOURCE_BRANCH, params.TARGET_BRANCH, params.CLEAN_WORKSPACE)
|
|
|
|
// building sqlcipher is not parallelizable on the same directory and doesn't lock; so we
|
|
// pre-build it while we're not parallel yet. the browser and node tests will pick up the binary
|
|
// instead of each building it. we can parallelize it with build-packages though.
|
|
sh '''
|
|
node buildSrc/getNodeGypLibrary.js @signalapp/sqlcipher --copy-target node_sqlcipher --environment node --root-dir . &
|
|
PID1=$!
|
|
npm run build-packages &
|
|
PID2=$!
|
|
wait $PID1
|
|
EXIT_CODE1=$?
|
|
wait $PID2
|
|
EXIT_CODE2=$?
|
|
exit $(node -p "$EXIT_CODE1 + $EXIT_CODE2")
|
|
'''
|
|
duplicateWorkspace(linuxWorkspace, linuxWorkspaceClones)
|
|
}
|
|
}
|
|
stage("checkout mac m1") {
|
|
agent {
|
|
node {
|
|
label "mac-m1"
|
|
customWorkspace macWorkspace
|
|
}
|
|
}
|
|
steps {
|
|
initWorkspace(changeset, params.SOURCE_BRANCH, params.TARGET_BRANCH, params.CLEAN_WORKSPACE)
|
|
prepareSwift()
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
stage("Lint and Style") {
|
|
parallel {
|
|
stage("find FIXMEs") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[0]
|
|
}
|
|
}
|
|
steps {
|
|
findFixmes()
|
|
}
|
|
}
|
|
stage("lint:check") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[1]
|
|
}
|
|
}
|
|
steps {
|
|
sh 'npm run lint:check'
|
|
}
|
|
}
|
|
stage("style:check") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[2]
|
|
}
|
|
}
|
|
steps {
|
|
sh 'npm run style:check'
|
|
}
|
|
}
|
|
stage("check rust formatting") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[3]
|
|
}
|
|
}
|
|
// this is so quick, we can run it every time.
|
|
steps {
|
|
sh "cargo fmt --check"
|
|
}
|
|
}
|
|
stage("lint swift") {
|
|
agent {
|
|
node {
|
|
label "mac-m1"
|
|
customWorkspace macWorkspace
|
|
}
|
|
}
|
|
when {
|
|
expression { extensionChanged(changeset, ".swift") }
|
|
}
|
|
steps {
|
|
lock("ios-build-m1") {
|
|
lintSwift()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stage("Testing and Building") {
|
|
parallel {
|
|
stage("tests") {
|
|
// the test cases write to the same files in the common workspace which produces race conditions like wasm/malloc function not available. as a workaround we run the critical tests sequentially. ideally we should never write to a re-used workspace.
|
|
stages {
|
|
stage("node tests") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[1]
|
|
}
|
|
}
|
|
steps {
|
|
sh 'cd test && node test'
|
|
}
|
|
}
|
|
stage("browser tests") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[2]
|
|
}
|
|
}
|
|
steps {
|
|
sh 'npm run test:app -- --no-run --browser --browser-cmd \'$(which chromium) --no-sandbox --enable-logging=stderr --headless=new --disable-gpu\''
|
|
}
|
|
}
|
|
stage("android tests") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspace
|
|
}
|
|
}
|
|
when {
|
|
expression { hasRelevantChangesIn(changeset, "app-android") }
|
|
}
|
|
steps {
|
|
testAndroid()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stage("packages test") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[0]
|
|
}
|
|
}
|
|
when {
|
|
expression { hasRelevantChangesIn(changeset, "packages") }
|
|
}
|
|
steps {
|
|
sh 'npm run --if-present test -ws'
|
|
}
|
|
}
|
|
stage("build web app") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[3]
|
|
}
|
|
}
|
|
steps {
|
|
sh 'node webapp --disable-minify'
|
|
}
|
|
}
|
|
stage("build web app calendar") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[4]
|
|
}
|
|
}
|
|
steps {
|
|
sh 'node webapp --disable-minify --app calendar'
|
|
}
|
|
}
|
|
stage("sdk tests") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[5]
|
|
}
|
|
}
|
|
when {
|
|
expression { hasRelevantChangesIn(changeset, "tuta-sdk") }
|
|
}
|
|
steps {
|
|
// once we spin local http server, we should also include more test by:
|
|
// --features test-with-local-http-server
|
|
sh "cargo test --package tuta-sdk"
|
|
}
|
|
}
|
|
|
|
stage("clippy lints") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspaceClones[6]
|
|
}
|
|
}
|
|
when {
|
|
expression { extensionChanged(changeset, ".rs", ".toml") }
|
|
}
|
|
steps {
|
|
// -Dwarnings changes warnings to errors so that the check fails
|
|
sh "cargo clippy --all --no-deps -- -Dwarnings"
|
|
}
|
|
}
|
|
stage("ios tests") {
|
|
// iOS tests write to the same temp folder which can cause both tests to fail, so they must run sequentially
|
|
stages {
|
|
stage("ios app") {
|
|
agent {
|
|
node {
|
|
label "mac-m1"
|
|
customWorkspace macWorkspace
|
|
}
|
|
}
|
|
when {
|
|
expression { hasRelevantChangesIn(changeset, "app-ios") }
|
|
}
|
|
steps {
|
|
lock("ios-build-m1") {
|
|
testFastlane("test_tuta_app")
|
|
}
|
|
}
|
|
}
|
|
stage("ios framework") {
|
|
agent {
|
|
node {
|
|
label "mac-m1"
|
|
customWorkspace macWorkspace
|
|
}
|
|
}
|
|
when {
|
|
expression { hasRelevantChangesIn(changeset, "app-ios") }
|
|
}
|
|
steps {
|
|
lock("ios-build-m1") {
|
|
testFastlane("test_tuta_shared_framework")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stage("finalize") {
|
|
agent {
|
|
node {
|
|
label 'linux'
|
|
customWorkspace linuxWorkspace
|
|
}
|
|
}
|
|
steps {
|
|
finalize(params.DRY_RUN)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void assertBranchWasSet(String name, String param) {
|
|
if (param.contains("dummy-do-not-use")) {
|
|
error("Parameter ${name} must be set to a valid branch name, not '${param}'.")
|
|
}
|
|
}
|
|
|
|
void initWorkspace(HashSet<String> changeset, String srcBranch, String targetBranch, boolean shouldClean) {
|
|
if (shouldClean) {
|
|
sh """
|
|
git submodule deinit --all --force
|
|
git clean -dfx
|
|
git fetch
|
|
git reset --hard origin/${srcBranch}
|
|
cargo clean
|
|
"""
|
|
}
|
|
sh "git status && git remote -v"
|
|
fetch(srcBranch, targetBranch)
|
|
merge(srcBranch, targetBranch)
|
|
submodules()
|
|
sh "pwd && git status && git remote -v && git submodule status"
|
|
getChangeset(changeset, targetBranch)
|
|
if (shouldRunNpmCi()) {
|
|
sh "npm ci"
|
|
}
|
|
}
|
|
|
|
void duplicateWorkspace(String source, ArrayList<String> targets) {
|
|
// don't trust the jenkins primitives to do this correctly...
|
|
sh """
|
|
pwd
|
|
cd ${source}/..
|
|
TARGETS="${targets.join(" ")}"
|
|
ls -halt
|
|
rm -f \$TARGETS
|
|
ls -halt
|
|
for target in \${TARGETS}; do
|
|
ln -s ${source} \$target
|
|
done
|
|
ls -halt
|
|
"""
|
|
}
|
|
|
|
void fetch(String srcBranch, String targetBranch) {
|
|
sh """
|
|
git switch --detach HEAD~1
|
|
git branch -D ${targetBranch} ${srcBranch} || true
|
|
git fetch
|
|
git switch ${srcBranch}
|
|
git switch ${targetBranch}
|
|
"""
|
|
}
|
|
|
|
void submodules() {
|
|
sh """
|
|
git submodule init
|
|
git submodule sync --recursive
|
|
git submodule update
|
|
"""
|
|
}
|
|
|
|
void prepareSwift() {
|
|
sh '''
|
|
mkdir -p ./build-calendar-app
|
|
mkdir -p ./build
|
|
|
|
cd app-ios
|
|
xcodegen --spec calendar-project.yml
|
|
xcodegen --spec mail-project.yml
|
|
|
|
cd ../tuta-sdk/ios
|
|
xcodegen
|
|
'''
|
|
}
|
|
|
|
void lintSwift() {
|
|
sh '''
|
|
cd app-ios
|
|
./lint.sh lint:check
|
|
./lint.sh style:check
|
|
'''
|
|
}
|
|
|
|
void merge(String srcBranch, String targetBranch) {
|
|
def sucessfulMerge = sh(returnStatus: true, script: "git merge --ff-only ${srcBranch}").toInteger()
|
|
if (sucessfulMerge != 0) {
|
|
error("Failed to merge branch ${srcBranch} into ${targetBranch}. Please rebase and try again.")
|
|
}
|
|
}
|
|
|
|
// must be called while checked out on targetBranch, after ff-merging srcBranch.
|
|
void getChangeset(HashSet<String> changeset, String targetBranch) {
|
|
def out = sh(returnStdout: true, script: "git diff --name-only ${targetBranch} origin/${targetBranch} --")
|
|
def lines = out.split('\n')
|
|
for (String line in lines) {
|
|
changeset.add(line.trim())
|
|
}
|
|
println "changeset:\n\n${changeset.join("\n")}"
|
|
}
|
|
|
|
// return whether any file in the given paths changed (recursively)
|
|
boolean hasRelevantChangesIn(HashSet<String> changeset, String... paths) {
|
|
boolean relevant = false
|
|
for (String path in paths) {
|
|
relevant = relevant || changeset.any { f -> f.startsWith(path) }
|
|
}
|
|
|
|
return relevant || extensionChanged(changeset, "groovy") || params.FORCE_RUN_ALL
|
|
}
|
|
|
|
// return whether any file with the given extensions changed
|
|
boolean extensionChanged(HashSet<String> changeset, String... exts) {
|
|
boolean changed = false
|
|
for (String ext in exts) {
|
|
changed = changed || changeset.any { f -> f.endsWith(ext) }
|
|
}
|
|
return changed || params.FORCE_RUN_ALL
|
|
}
|
|
|
|
boolean shouldRunNpmCi() {
|
|
def current = readFile(file: 'package.json')
|
|
def old
|
|
try {
|
|
old = readFile(file: 'cache/package.json')
|
|
} catch (e) {
|
|
print "package.json not found in cache, re-caching."
|
|
print e
|
|
return true
|
|
} finally {
|
|
writeFile(file: 'cache/package.json', text: current)
|
|
}
|
|
def json = new JsonSlurper()
|
|
def oldJson = json.parseText(old)
|
|
def currentJson = json.parseText(current)
|
|
def oldVersion = oldJson.version
|
|
def currentVersion = currentJson.version
|
|
def oldWithUpdatedVersion = old.replaceAll(oldVersion, currentVersion)
|
|
def expectedJSON = json.parseText(oldWithUpdatedVersion)
|
|
if (expectedJSON.equals(currentJson)) {
|
|
print "skipping npm ci as package.json is unchanged"
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
void findFixmes() {
|
|
sh '''
|
|
if grep "FIXME\\|[fF]ixme" -r src buildSrc test/tests packages/*/lib app-android/app/src app-ios/tutanota/Sources tuta-sdk; then
|
|
echo 'FIXMEs in src';
|
|
exit 1;
|
|
else
|
|
echo 'No FIXMEs in src';
|
|
fi
|
|
'''
|
|
}
|
|
|
|
void testAndroid() {
|
|
sh '''
|
|
# We have some tests for same day alarms that depends on this TimeZone
|
|
export TZ=Europe/Berlin
|
|
mkdir -p build
|
|
mkdir -p build-calendar-app
|
|
cd app-android
|
|
./gradlew lint -PtargetABI=arm64 --quiet
|
|
./gradlew test -PtargetABI=arm64
|
|
'''
|
|
}
|
|
|
|
void testFastlane(String task) {
|
|
sh """
|
|
export LC_ALL="en_US.UTF-8"
|
|
export LANG="en_US.UTF-8"
|
|
cd app-ios
|
|
fastlane ${task}
|
|
"""
|
|
}
|
|
|
|
/**
|
|
* push the resulting repo state to origin if it's not a dry run
|
|
*/
|
|
void finalize(boolean dryRun) {
|
|
if (dryRun) {
|
|
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
|
|
sh "exit 1"
|
|
}
|
|
echo """everything is fine, but I'm not pushing (DRY_RUN)!
|
|
use the following link to re-run and merge:
|
|
https://next.tutao.de/jenkins/job/${env.JOB_NAME}/parambuild?TARGET_BRANCH=${params.TARGET_BRANCH}&SOURCE_BRANCH=${params.SOURCE_BRANCH}&CLEAN_WORKSPACE=false&DRY_RUN=false
|
|
"""
|
|
} else {
|
|
sh "git push origin HEAD:${params.TARGET_BRANCH}"
|
|
}
|
|
}
|