196 lines
8.9 KiB
Groovy
196 lines
8.9 KiB
Groovy
// cdHookRunner — standalone Jenkins job that runs turbo-turtle deploy-phase
|
|
// CI/CD hooks (pre_deploy / post_deploy) in an isolated agent pod.
|
|
//
|
|
// turbo-turtle triggers this parameterized job during the CD workflow (see
|
|
// TriggerCdHookPipelineActivity). It checks out the target repo, reads the
|
|
// deploy-phase hooks declared under `environment.<TT_ENV>.hooks.<HOOK_PHASE>` in
|
|
// the per-app deployment file (deployments/<APP_NAME>.yaml), executes them via
|
|
// the shared runHooks execution core, and POSTs an aggregate completion callback
|
|
// to /api/v1/cd/hook/callback which signals the waiting workflow. A blocking hook
|
|
// failure makes success=false so the workflow can fail the deploy; advisory
|
|
// (blocking:false) failures keep success=true.
|
|
//
|
|
// Required parameters (Jenkins job params, read from params/env):
|
|
// REPO_URL, TT_REPO, APP_NAME, BRANCH, COMMIT, TT_ENV, HOOK_PHASE,
|
|
// IMAGE_TAG, TT_IS_HOTFIX, TT_PR_NUMBER, TT_WORKFLOW_ID, TT_RUN_ID
|
|
def call(Map jobParams = [:]) {
|
|
String repoUrl = param('REPO_URL', jobParams)
|
|
String repoName = param('TT_REPO', jobParams)
|
|
String appName = param('APP_NAME', jobParams)
|
|
String branch = param('BRANCH', jobParams)
|
|
String commit = param('COMMIT', jobParams)
|
|
String ttEnv = param('TT_ENV', jobParams)
|
|
String phase = param('HOOK_PHASE', jobParams)
|
|
String workflowID = param('TT_WORKFLOW_ID', jobParams)
|
|
String runID = param('TT_RUN_ID', jobParams)
|
|
|
|
if (!repoUrl && repoName) {
|
|
repoUrl = "https://github.com/Meesho/${repoName}"
|
|
}
|
|
// TT_WORKFLOW_ID / TT_RUN_ID are required: the completion callback correlates
|
|
// to the waiting Temporal run by them. If either is missing (e.g. incomplete
|
|
// job-param registration), fail before checkout/hook execution rather than run
|
|
// hooks whose callback can never be matched (workflow would wait to timeout).
|
|
if (!repoName || !appName || !ttEnv || !phase || !workflowID || !runID) {
|
|
error("cdHookRunner: TT_REPO, APP_NAME, TT_ENV, HOOK_PHASE, TT_WORKFLOW_ID and TT_RUN_ID are required parameters.")
|
|
}
|
|
// Make env available to stageName / the hook context.
|
|
env.cicd_environment = ttEnv
|
|
|
|
String podyaml = "org/meesho/${env.INFRA_ENV ?: 'prd'}-pod.yaml"
|
|
podTemplate(yaml: libraryResource(podyaml)) {
|
|
node(POD_LABEL) {
|
|
container('devops-tools') {
|
|
boolean success = true
|
|
String errMsg = ''
|
|
try {
|
|
stage(stageName("cd-hook checkout: ${repoName}")) {
|
|
deleteDir()
|
|
dir(repoName) {
|
|
checkout([
|
|
$class: 'GitSCM',
|
|
branches: [[name: commit ?: "*/${branch}"]],
|
|
userRemoteConfigs: [[url: repoUrl, credentialsId: 'svc-devops-meesho']],
|
|
extensions: [[$class: 'CloneOption', shallow: false, noTags: false]],
|
|
])
|
|
}
|
|
}
|
|
|
|
def hooks = resolveHooks(repoName, appName, ttEnv, phase)
|
|
if (!hooks) {
|
|
log.info("cdHookRunner: no ${phase} hooks declared for env ${ttEnv} in ${repoName}/deployments/${appName}.yaml — nothing to run.")
|
|
} else {
|
|
List<String> ctxEnv = deployPhaseContextEnv(repoName, ttEnv, jobParams)
|
|
runHooks.executeHooks(repoName, hooks, phase, ctxEnv)
|
|
}
|
|
} catch (Exception e) {
|
|
success = false
|
|
errMsg = e.toString()
|
|
log.error("cdHookRunner: ${phase} hook run failed for ${repoName} (env ${ttEnv}): ${errMsg}")
|
|
} finally {
|
|
postCdHookCallback(repoName, ttEnv, phase, success, errMsg, workflowID, runID)
|
|
}
|
|
if (!success) {
|
|
// Surface as a build failure too (the workflow reads the callback,
|
|
// but a red build aids debugging).
|
|
currentBuild.result = 'FAILURE'
|
|
error("cdHookRunner: ${phase} hooks failed for ${repoName}: ${errMsg}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolveHooks reads the per-app deployment file (deployments/<appName>.yaml) and
|
|
// returns the deploy-phase hook list for env+phase (or null). Deploy-phase hooks
|
|
// live in deployment.yaml, not config.yaml.
|
|
def resolveHooks(String repoName, String appName, String ttEnv, String phase) {
|
|
dir(repoName) {
|
|
String deployFile = "deployments/${appName}.yaml"
|
|
if (!fileExists(deployFile)) {
|
|
error("cdHookRunner: ${deployFile} not found in ${repoName}.")
|
|
}
|
|
def deployment = readYaml file: deployFile
|
|
def envBlock = deployment?.environment?.get(ttEnv)
|
|
def hooksBlock = envBlock?.hooks
|
|
if (!(hooksBlock instanceof Map)) {
|
|
return null
|
|
}
|
|
def hooks = hooksBlock[phase]
|
|
return (hooks instanceof List && !hooks.isEmpty()) ? hooks : null
|
|
}
|
|
}
|
|
|
|
// deployPhaseContextEnv builds the TT_* context contract from the job params.
|
|
def deployPhaseContextEnv(String repoName, String ttEnv, Map jobParams) {
|
|
return [
|
|
"TT_REPO_NAME=${repoName}",
|
|
"TT_ENV=${ttEnv}",
|
|
"TT_EVENT=push",
|
|
"TT_IS_HOTFIX=${param('TT_IS_HOTFIX', jobParams) ?: 'false'}",
|
|
"TT_BRANCH=${param('BRANCH', jobParams) ?: ''}",
|
|
"TT_TARGET_BRANCH=",
|
|
"TT_PR_NUMBER=${param('TT_PR_NUMBER', jobParams) ?: ''}",
|
|
"TT_COMMIT_SHA=${param('COMMIT', jobParams) ?: ''}",
|
|
"TT_IMAGE_TAG=${param('IMAGE_TAG', jobParams) ?: ''}",
|
|
]
|
|
}
|
|
|
|
// postCdHookCallback POSTs the aggregate result to turbo-turtle, which signals
|
|
// the waiting CD workflow. Env-routed exactly like the Jenkins CI callback.
|
|
def postCdHookCallback(String repoName, String ttEnv, String phase, boolean success, String errMsg, String workflowID, String runID) {
|
|
String baseUrl
|
|
switch (ttEnv) {
|
|
case 'prd':
|
|
case 'int':
|
|
baseUrl = 'http://turbo-turtle.meeshogcp.in'
|
|
break
|
|
default:
|
|
baseUrl = 'http://turbo-turtle.admin.meeshogcp.in'
|
|
}
|
|
Map payload = [
|
|
repo_name : repoName,
|
|
env : ttEnv,
|
|
phase : phase,
|
|
success : success,
|
|
error : errMsg,
|
|
build_url : env.BUILD_URL ?: '',
|
|
workflow_id: workflowID,
|
|
run_id : runID,
|
|
]
|
|
String url = baseUrl + "/api/v1/cd/hook/callback"
|
|
String jsonFilePath = "cd_hook_callback_${env.BUILD_NUMBER}.json"
|
|
int maxAttempts = 3
|
|
// The callback is turbo-turtle's ONLY completion signal for the waiting run.
|
|
// Retry delivery (bounded, with connect/request timeouts) and, if every attempt
|
|
// fails, fail the job so the failure is visible — otherwise the job goes green
|
|
// while the workflow waits until its backstop timeout. Duplicate delivery is
|
|
// safe: turbo-turtle correlates callbacks by workflow/run/repo/env/phase.
|
|
try {
|
|
writeFile(file: jsonFilePath, text: writeJSON(returnText: true, json: payload))
|
|
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
boolean delivered = false
|
|
try {
|
|
final def(String response, String code) = sh(
|
|
returnStdout: true,
|
|
script: """
|
|
curl -s --connect-timeout 10 --max-time 30 -X POST \\
|
|
-H 'Content-Type: application/json' \\
|
|
-w '\\n%{response_code}' \\
|
|
$url \\
|
|
-d @$jsonFilePath
|
|
"""
|
|
).trim().tokenize("\n")
|
|
if (code == "200") {
|
|
log.info("cdHookRunner: callback delivered (attempt ${attempt}/${maxAttempts})")
|
|
delivered = true
|
|
} else {
|
|
log.warn("cdHookRunner: callback attempt ${attempt}/${maxAttempts} failed code=${code} response=${response}")
|
|
}
|
|
} catch (Exception e) {
|
|
log.warn("cdHookRunner: callback attempt ${attempt}/${maxAttempts} error: ${e}")
|
|
}
|
|
if (delivered) {
|
|
return
|
|
}
|
|
if (attempt < maxAttempts) {
|
|
sleep(time: attempt * 5, unit: 'SECONDS')
|
|
}
|
|
}
|
|
error("cdHookRunner: callback POST to ${url} failed after ${maxAttempts} attempts; turbo-turtle will not receive completion for repo=${repoName} env=${ttEnv} phase=${phase}")
|
|
} finally {
|
|
sh(script: "rm -f ${jsonFilePath}", returnStatus: true)
|
|
}
|
|
}
|
|
|
|
// param reads a job parameter, preferring an explicit map, then params, then env.
|
|
def param(String key, Map jobParams) {
|
|
if (jobParams?.containsKey(key)) {
|
|
return jobParams[key]?.toString()
|
|
}
|
|
if (params?.containsKey(key) && params[key] != null) {
|
|
return params[key].toString()
|
|
}
|
|
return env[key]?.toString()
|
|
}
|