pipeline {
// 에이전트 설정 (Mac 머신 사용)
agent { label 'mac' }
// SCM Polling 트리거 설정 (금요일마다 5분마다 체크)
triggers {
pollSCM('H/5 * * * 5') // "매주 금요일 5분마다 SCM 변경 체크"
}
// 파라미터 정의
parameters {
string(name: 'PRJ_URL', defaultValue: '<https://github.com/Geekble-Game/SerpentOfTheEnd-client.git>', description: 'GitHub repository URL') // Git URL
string(name: 'BRANCH', defaultValue: 'develop', description: 'Branch to build') // 브랜치명
string(name: 'UNITY_VER', defaultValue: '6000.0.26f1', description: 'Unity Editor version') // 유니티 버전
string(name: 'BUILD_ROOT', defaultValue: '/Users/msg/Desktop/build', description: 'Build output root directory') // 빌드 결과 저장 경로
booleanParam(name: 'UNITY_BUILD_AAB', defaultValue: true, description: 'Build Android App Bundle (AAB)') // AAB 여부
}
// 환경 변수 설정
environment {
UNITY_PATH = "/Applications/Unity/Hub/Editor/${params.UNITY_VER}/Unity.app/Contents/MacOS/Unity"
}
// 빌드 옵션
options {
disableConcurrentBuilds() // 중복 빌드 방지
}
// 빌드 단계 (Stages)
stages {
// 1단계: Clean Workspace
stage('Clean Workspace') {
steps {
cleanWs()
}
}
// 2단계: Git Checkout (서브모듈 포함)
stage('Checkout with Submodules') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: "*/${params.BRANCH}"]],
userRemoteConfigs: [[
url: "${params.PRJ_URL}",
credentialsId: 'SerpentOfTheEnd_Jenkins_Polling_TOKEN'
]],
extensions: [
[$class: 'SubmoduleOption', recursiveSubmodules: true, trackingSubmodules: true],
[$class: 'RelativeTargetDirectory', relativeTargetDir: 'SerpentOfTheEnd-client']
]
])
}
}
// 3단계: 프로젝트명 초기화
stage('Init Project Name') {
steps {
script {
def (repoName, projectName) = parseProjectInfoFromRepoUrl(params.PRJ_URL)
env.BUILD_REPO_NAME = repoName
env.BUILD_PROJECT_NAME = projectName
echo "[INFO] Git URL: ${params.PRJ_URL}"
echo "[INFO] REPO Name: ${env.BUILD_REPO_NAME}"
echo "[INFO] Project Name: ${env.BUILD_PROJECT_NAME}"
echo "[INFO] Branch Name: ${params.BRANCH}"
}
}
}
// 4단계: 유니티 버전 확인
stage('Unity Version Check') {
steps {
sh """
"${env.UNITY_PATH}" -version
"""
}
}
// 5단계: Unity Android 빌드
stage('Build Unity Android') {
options {
timeout(time: 1, unit: 'HOURS') // 1시간 제한
}
steps {
withCredentials([
string(credentialsId: 'keystore_pass', variable: 'KEYSTORE_PASS'),
string(credentialsId: 'key_alias', variable: 'KEY_ALIAS'),
string(credentialsId: 'key_alias_pass', variable: 'KEY_ALIAS_PASS')
]) {
script {
env.BUILD_VERSION = getUnityVersionCode() // bundleVersion 추출
env.BUILD_NAME = getBuildName(env.BUILD_VERSION) // 빌드명 생성
env.DEFAULT_BUILD_PATH = "${env.BUILD_PROJECT_NAME}/${params.BRANCH}/${env.BUILD_VERSION}" // 빌드 경로
// 출력/로그 경로 설정
def localBuildPath = "${params.BUILD_ROOT}/${env.DEFAULT_BUILD_PATH}"
def outputDir = "${localBuildPath}/Builds/android"
def logDir = "${localBuildPath}/Logs"
def logPath = "${logDir}/${env.BUILD_NAME}.log"
def ext = (params.UNITY_BUILD_AAB) ? 'aab' : 'apk'
def filePath = "${outputDir}/${env.BUILD_NAME}.${ext}"
def projectPath = "${env.WORKSPACE}/${env.BUILD_REPO_NAME}/${env.BUILD_PROJECT_NAME}"
def keystorePath = "${projectPath}.keystore"
// 환경 변수 export
env.BUILD_OUTPUT_PATH = filePath
env.BUILD_LOG_PATH = logPath
withEnv([
"UNITY_BUILD_PATH=${filePath}",
"UNITY_BUILD_AAB=${params.UNITY_BUILD_AAB}",
"KEYSTORE_PATH=${keystorePath}",
"KEYSTORE_PASS=${env.KEYSTORE_PASS}",
"KEY_ALIAS=${env.KEY_ALIAS}",
"KEY_ALIAS_PASS=${env.KEY_ALIAS_PASS}"
]) {
// 빌드 실행
sh """
mkdir -p "${logDir}"
"${env.UNITY_PATH}" \
-batchmode \
-nographics \
-projectPath "${projectPath}" \
-executeMethod AndroidBuilder.BuildAndroid \
-quit \
-logFile "${logPath}" \
-buildTarget android
"""
}
// 빌드 결과 확인
if (!fileExists(filePath)) {
error "[ERROR] Build failed: ${filePath} not found"
}
}
}
}
}
// 6단계: S3 업로드
stage('Upload to S3') {
steps {
withCredentials([
[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'AWS_CREDENTIALS']
]) {
script {
env.BUILD_S3_URL = uploadToS3(env.BUILD_OUTPUT_PATH, env.DEFAULT_BUILD_PATH)
}
}
}
}
}
// Post 빌드 작업 (성공 / 실패)
post {
// 성공 시
success {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'AWS_CREDENTIALS']]) {
script {
sendSlack("success", "#36a64f") // Slack 성공 메시지
}
}
}
// 실패 시
failure {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'AWS_CREDENTIALS']]) {
script {
env.BUILD_FAIL_LOG_URL = uploadToS3(env.BUILD_LOG_PATH, "${env.DEFAULT_BUILD_PATH}/logs") // 실패 로그 업로드
sendSlack("failure", "#FF0000") // Slack 실패 메시지
}
}
}
}
}
//
// 유틸 함수들
//
// Git URL → repoName, projectName 추출
def parseProjectInfoFromRepoUrl(repoUrl) {
def repoName = repoUrl.tokenize('/').last().replace('.git', '')
def projectName = repoName.replaceFirst(/-client$/, '')
return [repoName, projectName]
}
// bundleVersion 추출
def getUnityVersionCode() {
def versionFile = "${env.WORKSPACE}/${env.BUILD_REPO_NAME}/${env.BUILD_PROJECT_NAME}/ProjectSettings/ProjectSettings.asset"
if (!fileExists(versionFile)) {
error "[ERROR] ProjectSettings.asset not found: ${versionFile}"
}
def versionName = sh(
script: """
grep 'bundleVersion:' "${versionFile}" \\
| head -n 1 \\
| sed 's/.*:[ ]*//'
""",
returnStdout: true
).trim()
if (!versionName) {
error "[ERROR] bundleVersion not found in ${versionFile}"
}
echo "[INFO] Extracted VERSION_NAME: ${versionName}"
return "${versionName}"
}
// 빌드명 생성
def getBuildName(version) {
return "SE_${version}"
}
// S3 업로드
def uploadToS3(filePath, uploadPath, region = 'ap-northeast-2') {
def bucketName = "msg-game"
def s3Path = "s3://${bucketName}/${uploadPath}/android/"
def fileName = filePath.tokenize('/')[-1]
def s3Url = "<https://$>{bucketName}.s3.${region}.amazonaws.com/${uploadPath}/android/${fileName}"
sh """
export PATH=/usr/local/bin:/opt/homebrew/bin:\$PATH
aws s3 cp "${filePath}" "${s3Path}" --region ${region}
"""
echo "[INFO] Uploaded to S3: ${s3Url}"
return s3Url
}
// Slack 메시지 전송
def sendSlack(String status, String color) {
def title = (status == "success") ? "✅ *Unity Android 빌드 성공*" : "❌ *Unity Android 빌드 실패*"
def downloadUrl = (status == "success") ? env.BUILD_S3_URL : env.BUILD_FAIL_LOG_URL
def curlBody = """{
"text": "${title}",
"attachments": [{
"color": "${color}",
"fields": [
{"title": "Project", "value": "${env.BUILD_PROJECT_NAME}", "short": true},
{"title": "Branch", "value": "${params.BRANCH}", "short": true},
{"title": "Job", "value": "${env.JOB_NAME} #${env.BUILD_NUMBER}", "short": true},
{"title": "Download", "value": "${downloadUrl}", "short": false}
]
}]
}"""
withCredentials([string(credentialsId: 'SLACK_WEBHOOK_URL', variable: 'SLACK_URL')]) {
sh """
curl -X POST -H 'Content-type: application/json' --data '${curlBody}' "$SLACK_URL"
"""
}
}