diff --git a/.githooks/post-receive b/.githooks/post-receive index 7f75f18..c8b54d2 100644 --- a/.githooks/post-receive +++ b/.githooks/post-receive @@ -1,22 +1,5 @@ #!/bin/bash -# Only run for specific branches -read_branches() { - while IFS=' ' read -r _ _ remote_ref _; do - case "$remote_ref" in - refs/heads/testing|refs/heads/staging|refs/heads/main|refs/heads/production) - return 0 - ;; - esac - done - return 1 -} - -if ! read_branches; then - echo "Skipping deployment: not pushing testing, staging, main, or production." - exit 0 -fi - TIMESTAMP=$(date +%Y%m%d-%H%M%S) set -euo pipefail # -e: exit on error, -u: exit on unset var, -o pipefail: catch errors in pipes @@ -318,23 +301,42 @@ } } -ssh-add -l -set -x -# Main script execution loop -while read -r oldrev newrev ref; do - tmpdir=$(mktemp -d) - branch="${ref#refs/heads/}" - echo "--- Processing push for branch: $branch (from $oldrev to $newrev) ---" - if run_postreceive_tests "$branch" "$tmpdir"; then - echo "Tests passed for $branch. Proceeding with deployment." - deploy_expressjs_blog "$branch" "$tmpdir" "$oldrev" "$newrev" +# Only run for specific branches +read_branches() { + while IFS=' ' read -r _ _ remote_ref _; do + case "$remote_ref" in + refs/heads/testing|refs/heads/staging|refs/heads/main|refs/heads/production) + return 0 + ;; + esac + done + return 1 +} - # deploy_expressjs_blog "$branch" "$tmpdir" - echo "Deployment of $branch complete." - else - echo "Post-receive tests failed for $branch. Deployment aborted." - exit 1 +main() { + # Only deploy production, staging, testing, or main + if ! read_branches; then + echo "Skipping deployment: not pushing testing, staging, main, or production." + exit 0 fi -done -set +x + + set -x + # Main script execution loop + while read -r oldrev newrev ref; do + tmpdir=$(mktemp -d) + branch="${ref#refs/heads/}" + echo "--- Processing push for branch: $branch (from $oldrev to $newrev) ---" + if run_postreceive_tests "$branch" "$tmpdir"; then + echo "Tests passed for $branch. Proceeding with deployment." + deploy_expressjs_blog "$branch" "$tmpdir" "$oldrev" "$newrev" + + # deploy_expressjs_blog "$branch" "$tmpdir" + echo "Deployment of $branch complete." + else + echo "Post-receive tests failed for $branch. Deployment aborted." + exit 1 + fi + done + set +x +} diff --git a/JenkinsFile b/JenkinsFile index a19c24e..f274609 100644 --- a/JenkinsFile +++ b/JenkinsFile @@ -2,8 +2,10 @@ agent any environment { - DEPLOY_BASE = "/srv/jasonpoage.com" - GIT_REPO = 'git@git.jasonpoage.com:jason/expressjs-blog.git' + GIT_REPO = 'ssh://git@git.jasonpoage.com/jason/express-blog.git' + DEPLOY_BASE = '/srv/jasonpoage.com' + LOG_DIR = "${DEPLOY_BASE}/logs" + TIMESTAMP = sh(script: "date +%Y%m%d-%H%M%S", returnStdout: true).trim() } options { @@ -12,85 +14,200 @@ } parameters { - string(name: 'DEPLOY_BRANCH', defaultValue: 'main', description: 'Branch to deploy') + string(name: 'DEPLOY_BRANCH', defaultValue: 'main', description: 'Branch to deploy (testing, staging, main, production only)') + string(name: 'OLD_REV', defaultValue: '', description: 'Old git revision (optional)') + string(name: 'NEW_REV', defaultValue: '', description: 'New git revision (optional)') } stages { - stage('Checkout') { + stage('Validate Branch') { steps { - checkout scm + script { + def allowed = ['testing', 'staging', 'main', 'production'] + if (!allowed.contains(params.DEPLOY_BRANCH)) { + error "Branch '${params.DEPLOY_BRANCH}' is not allowed for deployment." + } + } } } - stage('Prepare Environment') { + stage('Clone to Tempdir') { + steps { + script { + env.TMPDIR = sh(script: "mktemp -d", returnStdout: true).trim() + sh """ + git clone --branch '${params.DEPLOY_BRANCH}' '${GIT_REPO}' '${TMPDIR}' + """ + } + } + } + + stage('Copy and Source .env') { steps { script { env.ENV_FILE = "${DEPLOY_BASE}/${params.DEPLOY_BRANCH}.env" - env.DEPLOY_PATH = "${DEPLOY_BASE}/expressjs-blog-${params.DEPLOY_BRANCH}" - env.LOG_FILE = "${DEPLOY_BASE}/logs/jenkins-${params.DEPLOY_BRANCH}-${env.BUILD_ID}.log" + env.LOG_FILE = "${LOG_DIR}/receive-${env.TIMESTAMP}.log" + + sh """ + cp '${ENV_FILE}' '${TMPDIR}/.env' + """ + + def envVars = sh( + script: "set -a && source '${TMPDIR}/.env' && env | grep -E '^(SERVER_SCHEMA|SERVER_DOMAIN)='", + returnStdout: true + ).trim().split("\n") + + envVars.each { + def (key, value) = it.tokenize('=') + env[key] = value + } } + } + } + + stage('Initialize Submodules') { + steps { sh """ - cp "$ENV_FILE" .env + git --git-dir='${TMPDIR}/.git' --work-tree='${TMPDIR}' submodule update --init --recursive """ } } stage('Install Dependencies') { steps { - sh ''' - if git diff --name-only HEAD~1 | grep -qE "(package.json|yarn.lock)"; then - echo "Dependency files changed. Running yarn..." - yarn - else - echo "Skipping yarn install." - fi - ''' - } - } + script { + def skipInstall = false + if (params.OLD_REV && params.NEW_REV && params.OLD_REV != "0000000000000000000000000000000000000000") { + def changed = sh( + script: "git --git-dir='${TMPDIR}/.git' diff-tree --name-only -r ${params.OLD_REV}..${params.NEW_REV}", + returnStdout: true + ) + if (!changed.contains('package.json') && !changed.contains('yarn.lock')) { + skipInstall = true + } + } - stage('Initialize Submodules') { - steps { - sh ''' - git submodule update --init --recursive --force - ''' + if (!skipInstall) { + sh """ + cd '${TMPDIR}' + yarn + """ + } else { + echo "No dependency changes detected. Skipping yarn install." + } + } } } stage('Build CSS') { steps { - sh ''' + sh """ + cd '${TMPDIR}' yarn --production=false combine:css - ''' + """ + } + } + + stage('Start Application for Test') { + steps { + script { + env.PIDFILE = "${TMPDIR}/test.pid" + sh """ + systemctl --user stop express-blog@${params.DEPLOY_BRANCH}.service || true + cd '${TMPDIR}' + nohup node src/app.js >> '${LOG_FILE}' 2>&1 & + echo \$! > '${PIDFILE}' + """ + } + } + } + + stage('Wait for Service Readiness') { + steps { + script { + def timeout = 30 + def elapsed = 0 + def success = false + while (elapsed < timeout) { + def result = sh(script: "curl --silent --fail '${env.SERVER_SCHEMA}://${env.SERVER_DOMAIN}' > /dev/null || true", returnStatus: true) + if (result == 0) { + success = true + break + } + sleep 1 + elapsed += 1 + } + if (!success) { + sh "cat '${LOG_FILE}'" + error "Service did not become available within ${timeout}s." + } + } } } stage('Run Tests') { steps { - sh ''' - npm run test:postreceive - ''' + script { + def testStatus = sh(script: "cd '${TMPDIR}' && npm run test:postreceive", returnStatus: true) + if (testStatus != 0) { + sh "kill \$(cat '${PIDFILE}') || true" + sh "cat '${LOG_FILE}'" + error "Tests failed for branch ${params.DEPLOY_BRANCH}" + } + } + } + } + + stage('Stop Test App') { + steps { + sh "kill \$(cat '${PIDFILE}') || true" } } stage('Deploy') { steps { script { + def DEPLOY_PATH = "${DEPLOY_BASE}/expressjs-blog-${params.DEPLOY_BRANCH}" if (params.DEPLOY_BRANCH == 'testing') { sh """ - rm -rf "$DEPLOY_PATH" || true - mv "$WORKSPACE" "$DEPLOY_PATH" - ln -f "$ENV_FILE" "$DEPLOY_PATH/.env" + rm -rf '${DEPLOY_PATH}' || true + mv '${TMPDIR}' '${DEPLOY_PATH}' + git config -f ${DEPLOY_BASE}/expressjs-blog.git/modules/content/config core.worktree '${DEPLOY_BASE}/expressjs-blog-testing/content' + ln -f '${ENV_FILE}' '${DEPLOY_PATH}/.env' """ } else { + def dirExists = sh(script: "[ -d '${DEPLOY_PATH}' ] && echo 1 || echo 0", returnStdout: true).trim() + if (dirExists == "0") { + sh "git clone --branch '${params.DEPLOY_BRANCH}' '${GIT_REPO}' '${DEPLOY_PATH}'" + } + sh """ - mkdir -p "$DEPLOY_PATH" + cd '${DEPLOY_PATH}' git fetch origin - git reset --hard origin/${params.DEPLOY_BRANCH} + git reset --hard 'origin/${params.DEPLOY_BRANCH}' git submodule update --init --recursive --force - ln -f "$ENV_FILE" "$DEPLOY_PATH/.env" - yarn combine:css + ln -f '${ENV_FILE}' '${DEPLOY_PATH}/.env' """ + + def skipInstall = false + if (params.OLD_REV && params.NEW_REV && params.OLD_REV != "0000000000000000000000000000000000000000") { + def changed = sh( + script: "git --git-dir='${DEPLOY_PATH}/.git' diff-tree --name-only -r ${params.OLD_REV}..${params.NEW_REV}", + returnStdout: true + ) + if (!changed.contains('package.json') && !changed.contains('yarn.lock')) { + skipInstall = true + } + } + + if (!skipInstall) { + sh "cd '${DEPLOY_PATH}' && yarn" + } else { + echo "No dependency changes detected. Skipping yarn install." + } + + sh "cd '${DEPLOY_PATH}' && yarn combine:css" } } } @@ -98,19 +215,20 @@ stage('Restart Service') { steps { - sh """ - systemctl --user restart express-blog@${params.DEPLOY_BRANCH}.service - """ + sh "systemctl --user restart express-blog@${params.DEPLOY_BRANCH}.service" } } } post { - failure { - sh 'echo "Deployment failed for branch ${params.DEPLOY_BRANCH}"' - } success { - sh 'echo "Deployment succeeded for branch ${params.DEPLOY_BRANCH}"' + echo "Deployment of ${params.DEPLOY_BRANCH} completed successfully." + } + failure { + echo "Deployment of ${params.DEPLOY_BRANCH} failed." + } + cleanup { + sh "rm -rf '${TMPDIR}' || true" } } }