Kotlin Coverage Test – Static Analysis with Jenkins

Kotlin으로 모듈을 작성하다 보면, 모듈의 각 코드에 대한 단위 테스트와 코드 분석을 진행할 필요가 있다.

특히 그 모듈은 구(공 모양의 도형) 상에 존재하는 x개의 데이터 셋에 대해 y 를 제공했을 때, 데이터 셋 상에서 y에 최근접해있는 데이터를 찾는 모듈이었기 때문이었다.

그래서 모듈을 구성하는 모든 코드에 대한 단위 테스트를 진행하여 최대한 모듈 상의 버그를 없애고 (물론, 로직을 구성하는 최하위 알고리즘 상에 문제는 생길 수는 있다. 다만, 서울에서 멀리 떨어진 지역의 데이터이기 때문에 검증하기도 많이 어렵다.) 퍼포먼스, 보안 상 문제가 될 수 있는 모든 가능성을 없앨 필요가 있었다.

이 글에서는 그 과정에서 도입한 Spek Framework, Jacoco Report, Detekt 에 대해 알아보고, 이 세 개를 Jenkins에 연동하여 Jenkins dashboard 에서 해당 리포트에 대해 보여주는 과정을 살펴보려 한다.

Spek Framework

Spek Framework란 코틀린용 유닛 테스트 프레임워크로 Kotlin의 기여자들이 관리하는 프레임워크이다. (JetBrains 공식 프로젝트는 아니나, JetBrains 가 개발한 흔적은 있다.) 현재는 2.0.0-RC1 버전으로, 특징적인 점이라면 DSL로 유닛 테스트를 진행할 수 있다는 점이다.

class SomeTest: Spek({
    describe("Some test") {
        it("Check value is something") {
            // TEST BODY
        }
    }
})

이러한 구조를 가지고 있어 JUnit와 다른 테스트 프레임워크와는 다르게 쉬운 구조를 가지고 있다.

(이렇게 보면, Ktor도 그렇고 JetBrains 가 개발한 프레임워크들은 DSL를 매우 강조하는 것 같다. 과연 그것이 좋은지는 아직도 잘 모르겠긴 하다.)

임포트는 다음과 같이 진행한다.

buildscript {
    ext.kotlin_version = '1.3.11'
    ext.spek_version = '2.0.0-rc.1'
    ext.junit_version = '4.12'
​
    repositories {
        jcenter()
    }
​
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
​
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
    testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek_version"
    testRuntimeOnly "org.spekframework.spek2:spek-runner-junit5:$spek_version"
    testRuntimeOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
    testCompile("org.assertj:assertj-core:3.11.1")
    testCompile "junit:junit:$junit_version"
}
​
test {
    useJUnitPlatform {
        includeEngines 'spek2'
    }
}

이렇게 작성한 테스트 코드는 gradlew test 로 확인이 가능하다.

Jacoco Report

Jacoco Report는 Java Code Coverage를 구현하는 데 사용하는 툴킷으로, Line, Branch에 대한 Coverage를 제공한다.

임포트는 다음과 같이 한다.

apply plugin: 'jacoco'
​
test {
    useJUnitPlatform {
        includeEngines 'spek2'
    }
    jacoco {
        destinationFile = file("${buildDir}/jacoco/test.exec")
    }
}
​
test.finalizedBy(jacocoTestReport)
​
jacoco {
    // You may modify the Jacoco version here
    toolVersion = "0.8.2"
}
​
jacocoTestReport {
    // Adjust the output of the test report
    reports {
        xml.enabled true
        csv.enabled false
    }
}

gradlew test 로 테스트를 구동했을 경우, destinationFile로 제공한 경로에 exec 파일이 저장된다. 이 exec 파일은 Jacoco 가 생성한 리포트 파일이 포함되며, gradlew jacocoTestReport 명령어로 리포트 파일을 생성할 수 있다. (여기서는 xml, html 리포트를 생성한다.)

Detekt

Detekt는 코틀린에 대한 Static Analysis를 제공하는 툴킷으로, 코틀린으로 코드를 작성함에 있어 피해야하는 패턴들에 대해 감지, 특정 패턴들의 가중치를 파악해서 일정 이상이면 build 자체가 failed 되게 할 수 있어 코드 본연의 문제를 좀 더 파악할 수 있게 하는 툴킷이다.

특히 Codacy (https://www.codacy.com/) 에서 코틀린 프로젝트를 임포트 했을 때 사용하는 분석 툴킷으로, 많은 사람들에 의해 검증된 툴킷이기도 하다.

임포트 과정은 프로젝트 사이트(https://arturbosch.github.io/detekt/) 에 설명되어 있다.

detekt의 경우 gradlew detekt 로 통해 테스트를 진행할 수 있다. 이 때 report는 build/reports/detekt 에 detekt.html 에 저장되며 이 리포트 파일을 참고로 해서 파악할 수 있다.

Jenkins로 실행

위에서 살펴본 Spek Framework, Jacoco, Detekt를 빌드마다 실행하기 위해서 JenkinsFile에 해당 과정을 구현했다.

stage('Test Analysis') {
      parallel {
        stage('Static Analysis') {
          steps {
            sh './gradlew detekt'
            publishHTML(target: [reportDir:'build/reports/detekt/', reportFiles: 'detekt.html', reportName: 'Detekt report'])
          }
        }
        stage('Unit Test') {
          steps {
            sh './gradlew cleanTest test'
            sh './gradlew jacocoTestReport'
            publishHTML(target: [reportDir:'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: 'Code Coverage'])
          }
        }
      }
    }

gradlew build 가 끝난 다음, 이 단계를 실행하게 하면 된다. publishHTML은 특정 Html 파일을 Jenkins 메뉴 상에 표시해서 쉽게 볼 수 있게 한다.

Jenkins에 테스트 결과에 대한 Output 출력

Test Code를 작성할 때 println 메서드로 Test 콘솔에 Output를 출력할 수 있으나, 위 JenkinsFile를 가지고 실행하면 Output가 나오지 않는다.

이 때는 https://stackoverflow.com/a/36130467 링크에 있는 답변을 사용하면 된다.

최종 결과

전체 파이프라인 통과
각 테스트에 대해 PASSED / FAILED / SKIPPED 등이 표시된다. 만일 Output가 있을 경우 그 아래에 표시된다.
마지막에 총 결과가 표시된다.
Detekt 분석 결과는 콘솔에도 표시된다. 물론, 리포트에도 볼 수 있다.
Jenkins 의 메뉴에 Code Coverage, Detekt Report가 생긴 것을 확인할 수 있다.
Detekt가 생성한 report 파일이다.
Jacoco가 생성한 Coverage Report로, Line Coverage는 99%, Branch Coverage는 94%로 측정되었다.

마무리

제목은 다소 거창하지만, 실제로 한 행동은 그렇게 어렵지는 않다. 다만 이 글에서 사용한 프로젝트는 순수 Kotlin 프로젝트로 안드로이드 프로젝트는 아니다.

이 면에서 보면, 안드로이드 프로젝트에 대해 유닛 테스트를 진행하는 것은 다소 어렵기 때문에 조금 진입장벽이 높을 수는 있다고는 판단된다.

하지만 작은 것 부터 나아가면, 점점 더 나아질 것이라 생각한다.

새 인스턴스로 확장 이전과 후기

이번 연휴(2018. 12. 22 ~ 2018. 12. 25) 동안 지금까지 운영해오던 UzukiLive 서버를 확장이전 하여 새롭게 환경을 구축하게 되었습니다.

기존 인스턴스와 문제점

기존 환경은 이렇게 설정되어 있습니다.

  • Vultr VC2 1core 2GB (UzukiLive 인스턴스) -> 블로그, nextcloud. 도커로 되있지 않음
  • Vultr VC2 2core 4GB (Artifactory 인스턴스) -> Artifactory, Jenkins. 도커로 되어있음

그리고 메일 서버의 경우 Zoho 플랫폼을 빌려 사용하였고, 처음에 삽질을 너무 크게 한 나머지 uzuki.live 로 접속하면 SSL 에러가 발생하곤 했습니다.
그 외 문제점이라고 하면 FTP를 쉽게 사용할 수 없거나, 너무 설정이 여기저기 있어 확장하려고 해도 쉽지 않았습니다. (오죽하면 인스턴스를 두개 생성해서 관리를 했지만요… (._. )

당연히 비용도 비용이니 좋지 않은 사양에 월 30달러나 지불해야 한다는 것은 이해가 가지도 않긴 합니다.

따라서 이번 연휴때 계획을 잡고 신규 인스턴스에 전부 이전하기로 결정했습니다.

신규 인스턴스

신규 인스턴스는 Vultr의 VC2 4core 8GB, 100GB SSD 입니다. 이전 사양보다 약 2(Artifactory 인스턴스 기준) ~ 4(UzukiLive 인스턴스 기준)배 이상 사양이 증가하였습니다.

사양을 대폭 올린 이유로는 Jenkins 때문인데, 2core 4GB 사양에서 gradle 빌드를 두 개 이상 돌리는 순간 컨테이너가 메모리 부족으로 죽어버리는 대참사가 발생하여, 빌드를 두 개 이상 동시에 돌리더라도 문제가 없도록 구성하였습니다.

신규 인스턴스는 모든 요소가 Docker에 의해 관리되는 Dockerize 환경을 사용할 것이고, 아래 범주로 서비스를 관리할 예정입니다.

  • Reverse Proxy -> nginx
  • Stack: Infra -> adminer, pure-ftpd, docker-telegram-notifier
  • Stack: Blog -> MariaDB + WordPress
  • Stack: Build-Automation -> Jenkins, Artifactory
  • Stack: Mail -> docker-mailserver(IMAP/SMTP) + rainloop (Web UI)
  • Stack: Intro -> uzukilive-intropage
  • Stack: Octobox -> Octobox + Redis + PostgreSQL

그러면, 각 범주마다 설명을 약간 추가하면서 기능을 소개하려 합니다.

Reverse Proxy + SSL

기존 UzukiLive 에서도 사용했던 Nginx를 사용하고, 여기에 LetsEncrypt를 이용하여 모든 외부용 서비스에 SSL를 제공할 계획입니다.

보통 nginx-proxy + letsencrypt-companion 조합으로 사용하지만 이 조합의 경우 docker 소켓에 붙어 일정 시간마다 스캔을 해서 추가하는 방식으로 되어있어, 그거보다는 좀 더 귀찮지만 하나의 설정 파일(conf)로 존재하는 것이 좋다고 생각되었습니다.

따라서 새 서비스를 연동할 때 마다 아래의 명령어를 통해 인증서를 발급받았습니다.

docker run -it --rm -v /data/cert:/etc/letsencrypt -v /data/cert-data:/data/letsencrypt certbot/certbot certonly --webroot --webroot-path=/data/letsencrypt -d uzuki.live

저 방법을 사용하기 위해서는 일정의 예제 conf 파일을 만들어 nginx를 가동시킬 필요가 있는데, 샘플 nginx 파일을 만들고 DONAME_NAME 만 교체해서 생성하도록 했습니다.

server {
    listen      80;
    listen [::]:80;
    server_name uzuki.live;

    location / {
        rewrite ^ https://$host$request_uri? permanent;
    }

    location ^~ /.well-known {
        allow all;
        root  /data/letsencrypt/;
    }
}

Stack: Infra

여기에는 3개의 서비스가 들어가는데, adminer 는 MySQL/MariaDB의 정보를 보거나 수정할 수 있는 프로그램으로 phpmyadmin 대체용입니다

그 다음으로 pure-ftpd는 FTP 서버로 /home/pyxis 경로에 접근하여 데이터를 주고 받을 수 있게 설정했습니다.

마지막으로 docker-telegram-notifier 는 미리 만들어둔 Telegram bot로 컨테이너의 시작/중지 등의 상태를 보내줍니다.

가령 Jenkins가 시작되었다면 아래의 메세지를 보내줍니다.

Started container jenkins
Image: windsekirun/jenkins-android-docker:1.0.2
Container ID: ae8f526e8b42d216146fb68ee49f6506631259780f3c75195718786da6245cb6
오늘도 약 400개의 메세지를 받아내는 우즈키(..)

Stack: Blog

여기에는 2개의 서비스가 들어가는데, 워드프레스와 그 DB로 MariaDB를 선택했습니다. 기존 인스턴스에는 MySQL로 되어있고, 이 데이터를 그대로 가져갈 필요가 있었습니다.
따라서 PostgreSQL 보다는 MariaDB를 선택했습니다.

주소는 기존의 blog.uzuki.live 에서 pyxispub.uzuki.live 로 변경했는데, 기존 주소로 들어가면 새로운 주소로 리다이렉션 되도록 기존 인스턴스의 nginx 단을 수정했습니다.

기존 인스턴스는 2주 뒤인 1월 11일쯤에 제거될 예정입니다.

Stack: Build-Automation

여기에는 2개의 서비스가 들어가는데, 각각 Gradle Repository용 Artifactory와 Jenkins가 들어갑니다. 이 두개 서비스에 대해서는 일전 블로그 글로 설명한 적이 있으므로 별도로 설명할 것은 없다고 생각됩니다.

다만 아주 잉여롭지만 봇을 만든 겸에 빌드 시작 / 결과를 알려주는 별도의 Jenkins 호환용 그루비 스크립트를 작성해서 적용했습니다

Stack: Mail

기존 메일(pyxis@uzuki.live) 는 Zoho Mail에서 수신/발신하도록 되어있어 내 것이 아니기도 한 존재였습니다.
따라서 이번 인스턴스에는 IMAP/SMTP 서버를 직접 구축하여 메일을 수신/발신할 수 있도록 설정하였습니다.

…다만, vultr가 SMTP 포트인 25번을 막아버리는 바람에 일단은 Gmail에 의존하여 발신하도록 설정하였으나 좀 더 Postfix를 공부하여 수정할 계획입니다.

메인 화면
글쓰기 화면

웹 메일 UI는 rainloop를 사용하였고, 특출나게 UI가 세련된 건 아니지만 나름대로 들어갈 기능은 전부 포함되어 있습니다.

Stack: Intro

물론 반응형입니다.

CSS Toolkit인 타키온(https://tachyons.io/) 을 이용하여 5~10분만에 대충 만든 인트로 페이지로, 이 서버에서 제공하는 서비스의 링크와 Github, Telegram, Contact 링크를 포함합니다.

물론, 이 인트로 페이지(라고 해도 HTML 1개 + CSS 1개) 도 docker 이미지를 만들어서 제공했으며, 도커 이미지는 https://github.com/WindSekirun/uzukilive-intropage 입니다.

Stack: Octobox

Octobox 라고 하는 Github의 알림을 관리하는 서비스가 있는데, 이 서비스가 self hosted로 하면 프라이빗 프로젝트까지 포함되어 설치하게 되었습니다.

이 서비스의 자세한 설명은 공식 홈페이지 (https://octobox.io/) 를 들어가는 것이 편리합니다.

마무리

일전에도 Jenkins나 Artifactory를 도커로 활용하고 있었지만 이번 기회를 통해 도커를 통한 관리가 얼마나 확장성과 자유성을 가져주는지 깨달았습니다.

개인 목적으로는 나름대로 규모가 있고, 따로 설치하라고 한다면 아마 멘탈 여러번 나가서 쉽게 던졌을텐데, 명령어 한 두줄 만으로 모든 것을 설정할 수 있다는 것이 큰 장점인 것 같습니다.

컨테이너가 14개가 되다보니 docker-compose로 각 스택별로 모아 필요한 스택만 켜고 끄고 할 수 있게 설정해두니, 패치할 때도 다운타임이 5~10분 내로 할 수 있다는 것도 좋았습니다.

열심히 가동되는 컨테이너(들)

그러면, 새로운 인스턴스와 새로운 환경에서 남은 6일, 앞으로 다가올 2019년에도 PyxisPub와 이 주인장(._. … )을 잘 부탁드립니다.

Run Docker + Jenkins for Android Build

이번 글에서는 Vultr VC2 2core 4GB instance 에 Jenkins 를 올려 안드로이드 앱을 빌드하고 테스트하려 한다.

사용할 인스턴스는 기존에 사용중이던 Artifcatory 인스턴스이지만 사양을 올려 사용할 것이기 때문에, Docker 기본 설정 같은 것들은 이전 글인 ‘Upload Android Library into Gradle with Artifactory‘ 를 참조하면 된다.

DockerFile 커스텀하기

대부분 인터넷에 나온 CI/CD 적용기를 보면 이 단계부터 설명하는 글이 많은데, 처음 도커를 접하는 유저라면 상당히 골치아픈 작업이기도 하다. Dockerfile 자체가 자체 포맷으로 되어있기도 해서 그걸 익혀야 되는 문제점이 있다.

그리고 이번 글의 중점 취지는 아닌 것 같아 제작했던 Dockerfile 를 공유해서 바로 적용할 수 있게 했고, 그것이 WindSekirun/Jenkins-Android-Docker 이다.

DockerHub에도 공유되어 있으니, 바로 pulling 를 받으면 최신 안드로이드 환경 (API 28 + build tools 28.0.3) 을 사용할 수 있게 된다.

따라서 이번 글에서는 미리 제작된 도커 이미지로 이 과정을 대체한다.

먼저 실행되야 될 작업

본격적으로 이미지를 풀링 받기 전에, 이 이미지를 실행하기 위한 커맨드를 살펴볼 필요가 있다. (버전 1.0.1 같은 경우 2018-12-18 기준 최신으로, 가장 최신은 릴리즈 페이지를 참고하면 된다.)

sudo docker run -d -p 8080:8080 -p 50000:50000 -v /data/jenkins-android-docker:/var/jenkins_home windsekirun/jenkins-android-docker:1.0.1

여기에서 -d (백그라운드 작업) 과 -p(포트 바인딩) 은 넘겨도 되나 /data/jenkins-andorid-docker:/var/jenkins_home 부분에 신경을 써야 한다. 이 부분은 ‘실제 저장소내 공간:도커 컨테이너 공간’ 의 형식을 가지고 있는 디렉토리 바인딩 부분으로, 실제 저장소내 공간 부분에 작성한 폴더는 실제 존재하는 폴더여야 한다.

따라서 sudo 권한으로 아래 커맨드를 실행하면 된다.

mkdir /data/jenkins-android-docker
sudo chown -R 1000:1000 /data/jenkins-android-docker

이 ‘실제 저장소내 공간’에 모든 데이터가 들어가므로, 도커 컨테이너를 지워도 이 폴더가 남아있다면 데이터 또한 그대로 보존되게 된다.

위 mkdir 와 chown 을 시작했다면, 맨 위의 shell script 를 실행하면 된다.

귀찮은 사람들은 WindSekirun/Jenkins-Android-Docker 를 VPS 안에서 clone 받아서 sudo sh runImage.sh 를 실행하면 된다. mkdir 부터 docker run 까지 다 된다.

처음 관리자 설정하기

명령어로 도커를 시작했다면, 서버 주소:8080 으로 들어가면 Jenkins home 이 보일 것이다.

Jenkins 를 설정하는 사람이 관리자인지 확인하는 과정인데, 이를 확인하기 위해서는 도커 컨테이너에 접근할 필요가 있다.

SSH 에서 docker container ls 를 입력하면 현재 실행중인 컨테이너 정보가 나오는데, 그 곳에서 Jenkins-Android-Docker 를 찾는다.

위 정보에 따르면 jenkins-android-docker 가 설치된 컨테이너의 id는 d88376885153 이고, 이 컨테이너 id를 이용해 도커 컨테이너의 bash로 접근할 수 있다. 명령어는 docker exec -i -t [컨테이너 id] /bin/bash이다.

bash에 접근했으면 cat /var/jenkins_home/secrets/initialAdminPassword 로 도커 초기 관리자 비밀번호를 알아낸다.

아래 비밀번호를 복사해서 칸에 넣고 Continue 를 누르면 된다.

그 다음 Plugin 창이 나올텐데, 그 곳에서 Install Suggested Plugin를 누른다. 차후에 다시 설치가 가능하니, 지금은 기본만 설치한다.

설치가 다 되고, 관리자 계정을 만들면 Jenkins 를 사용할 준비가 모두 끝난다. 이제 blueocean 플러그인을 설치하여 첫 안드로이드 빌드를 해보도록 한다.

Blueocean 설치하기

Blueocean 은 Jenkins 에서 나온 새 UI/UX 툴로, 기존 Jenkins 가 다소 전문가의 영역에 가깝다고 하면 Blueocean 는 이를 좀 더 간결하고 알아보기 쉽게 만든 것이다.

Jenkins 메인에 접속되면 Manage Jenkins > Manage Plugin > Available 의 검색창에서 Blueocean 을 검색한다.

여기에서 Install without restart 를 누르면 플러그인 설치 페이지로 이동하는데, 여기에서 맨 마지막의 체크박스를 체크해서 바로 재시작될 수 있도록 한다.

여기까지 끝내면 첫 안드로이드 프로젝트를 빌드를 할 모든 준비가 완료된다.

프로젝트 빌드하기

다시 젠킨스 메인으로 돌아와서 옆의 Open Blueocean 을 누른다. 그러면 이제까지 보지 못했던 새로운 Jenkins 가 보이게 된다.

여기에서 ‘Create a new Pipeline’ 를 누른다.

프로젝트 저장소를 선택하고, 가져올 프로젝트를 맨 밑에서 설정한다. 만일 해당 프로젝트에 Jenkinsfile 가 없다면 설정하는 메뉴로 갈 것이고, 이미 있다면 바로 빌드를 시도할 것이다. 이번에는 Jenkins 로 연동해보지 않은 프로젝트를 설정했다.

그러면 이 페이지로 나오게 될텐데, 이 곳이 Jenkins 가 한 빌드당 거칠 파이프라인을 설정하는 곳이다. 가운데의 +를 누르게 되면 새 작업을 추가할 수 있다.


일단 여기에서는 간단히 빌드만 성공하는지 테스트할 것이므로, Add step 에서 Shell script 를 선택하고 ./gradlew assembleDebug --stacktrace를 입력해준다.

그 다음 위 Save를 누르면 파이프라인 저장 다이얼로그가 표시되고, Save&run 을 누르면 바로 빌드가 시작된다.

빌드 지켜보기

이제 프로젝트가 빌드될 때 까지 기다리는 것 만 남았다.

만일 오류가 나온다면 왜 오류가 나오는지 이제 구글링을 열심히 해볼 차례다. 아래는 지금까지 겪은 CI 오류를 정리해본 것이다.

흔한 오류

local.properties (No such file or directory)

assembleDebug 전에 ‘echo “sdk.dir=/opt/android-sdk-linux” >> local.properties’ 를 추가한다. 또는 젠킨스 관리 > Configure System > 맨 하단의 Android SDK Path 에 /opt/android-sdk-linux 를 적어준다.

File google-services.json is missing.

이 글을 참고하되 Environment Variable 를 Jenkins 내부에서 설정해주면 된다.

그리고 추가할 Shell script 는 echo $GOOGLE_SERVICES_JSON | base64 --decode --ignore-garbage > /app/google-services.json“` 이다.

Gradle build daemon disappeared unexpectedly

제일 골치아픈 문제로, 서버의 램 용량이 부족해서 Gradle 데몬이 죽는 현상이다. 이를 해결하기 위해서는 빌드 커맨드를 ./gradlew --no-daemon assembleDebug --stacktrace 로 설정하거나, 아니면 아예 메모리 제한을 거는 방법도 있다.

메모리 제한을 거는 방법은 현재 실행중인 컨테이너를 docker container kill [컨테이너 id]  – docker container stop [컨테이너 id] 로 삭제하고 (데이터는 상기했듯이 남아있다.) 컨테이너 실행할 때 -m 2500m 를 삽입한다. 2500m은 2.5g로 k, m, g 가 사용이 가능하다. 자신의 서버 환경에 맞게 적절히 조정하면 된다.

예제: sudo docker run -d -m 2500m -p 8080:8080 -p 50000:50000 -v /data/jenkins-android-docker:/var/jenkins_home windsekirun/jenkins-android-docker:1.0.1

빌드 성공

CI상 오류를 전부 해결하면 빌드 성공이 나오며, 이제야 첫 프로젝트의 빌드가 끝난 셈이다. 이제 다른 프로젝트를 연동하거나, 좀 더 심화해서 유닛 테스트나 마켓 업로드 기능 들을 구현하면 된다.

마지막으로 위 프로젝트의 빌드에 성공한 JenkinsFile는 다음과 같다. 다른 프로젝트의 루트 폴더에 똑같은 파일 이름으로 만들고 Jenkins 에서 추가하면 바로 인식이 된다.

pipeline {
  agent any
  stages {
    stage('Make Environment') {
      parallel {
        stage('Touch local.properties') {
          steps {
            sh 'echo "sdk.dir=/opt/android-sdk-linux" >> local.properties'
          }
        }
        stage('Touch google-services.json') {
          steps {
            sh 'echo $GOOGLE_SERVICES_JSON | base64 --decode --ignore-garbage > demo/google-services.json'
          }
        }
        stage('Display directory') {
          steps {
            sh 'ls -la'
          }
        }
      }
    }
    stage('assembleDebug') {
      steps {
        sh './gradlew --no-daemon assembleDebug --stacktrace'
      }
    }
  }
  environment {
    GOOGLE_SERVICES_JSON = ''
  }
}