pipeline {
agent { label 'mac' }
triggers {
pollSCM('* * * * *')
}
parameters {
string(name: 'PRJ_URL', defaultValue: '<https://github.com/Geekble-Game/SerpentOfTheEnd-client.git>', description: 'GitHub repository 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)')
}
environment {
UNITY_PATH = "/Applications/Unity/Hub/Editor/${params.UNITY_VER}/Unity.app/Contents/MacOS/Unity"
}
options {
disableConcurrentBuilds()
}
stages {
stage('Clean Workspace') {
steps {
cleanWs()
}
}
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']
]
])
}
}
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}"
}
}
}
stage('Unity Version Check') {
steps {
sh """
"${env.UNITY_PATH}" -version
"""
}
}
stage('Build Unity Android') {
options {
timeout(time: 1, unit: 'HOURS') // 🔥 Build 단계에만 timeout 적용
}
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 {
def defaultBuildPath = "${params.BUILD_ROOT}/${env.BUILD_PROJECT_NAME}/${params.BRANCH}"
def outputDir = "${defaultBuildPath}/Builds/android"
def logDir = "${defaultBuildPath}/Logs"
def buildName = getBuildName()
def ext = (params.UNITY_BUILD_AAB) ? 'aab' : 'apk'
def logPath = "${logDir}/${buildName}.log"
def filePath = "${outputDir}/${buildName}.${ext}"
def defaultProjectPath = "${env.WORKSPACE}/${env.BUILD_REPO_NAME}/${env.BUILD_PROJECT_NAME}"
def projectPath = "${defaultProjectPath}"
def keystorePath = "${defaultProjectPath}.keystore"
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"
}
}
}
}
}
stage('Upload to S3') {
steps {
withCredentials([
[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'AWS_CREDENTIALS']
]) {
script {
def bucketName = "msg-game"
def folderName = env.BUILD_PROJECT_NAME
def branchName = params.BRANCH
def region = "ap-northeast-2"
def s3Path = "s3://${bucketName}/${folderName}/${branchName}/android/"
def outputFile = env.BUILD_OUTPUT_PATH
def fileName = outputFile.tokenize('/')[-1]
def s3Url = "<https://$>{bucketName}.s3.${region}.amazonaws.com/${folderName}/${branchName}/android/${fileName}"
sh """
export PATH=/usr/local/bin:/opt/homebrew/bin:\$PATH
aws s3 cp "${outputFile}" "${s3Path}" --region ${region}
"""
env.BUILD_S3_URL = s3Url
echo "[INFO] S3 URL: ${env.BUILD_S3_URL}"
}
}
}
}
}
post {
success {
script {
sendSlack("success", "#36a64f")
}
}
failure {
script {
sendSlack("failure", "#FF0000")
}
}
}
}
def parseProjectInfoFromRepoUrl(repoUrl) {
def repoName = repoUrl.tokenize('/').last().replace('.git', '')
def projectName = repoName.replaceFirst(/-client$/, '')
return [repoName, projectName]
}
def getBuildName() {
def timestamp = sh(script: "date +\"%Y%m%d_%H%M%S\"", returnStdout: true).trim()
def gitHash = ''
def gitTag = ''
dir("${env.WORKSPACE}/${env.BUILD_REPO_NAME}") {
gitHash = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
gitTag = sh(script: "git describe --tags --abbrev=0 2>/dev/null || echo no-tag", returnStdout: true).trim()
}
return "app_${gitTag}_${gitHash}_${timestamp}"
}
def sendSlack(String status, String color) {
def title = (status == "success") ? "✅ *Unity Android 빌드 성공*" : "❌ *Unity Android 빌드 실패*"
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": "Build File", "value": "${env.BUILD_OUTPUT_PATH}", "short": false},
{"title": "S3 Download", "value": "${env.BUILD_S3_URL}", "short": false}
]
}]
}"""
withCredentials([string(credentialsId: 'SLACK_WEBHOOK_URL', variable: 'SLACK_URL')]) {
sh """
curl -X POST -H 'Content-type: application/json' --data '${curlBody}' "$SLACK_URL"
"""
}
}