diff --git a/.githooks/post-receive b/.githooks/post-receive index d252a2a..a4ca0da 100644 --- a/.githooks/post-receive +++ b/.githooks/post-receive @@ -1,342 +1,10 @@ #!/bin/bash - -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 - -get_deploy_env_file() { - branch="$1" - echo "/srv/jasonpoage.com/${branch}.env" -} - -get_deploy_path() { - branch="$1" - echo "/srv/jasonpoage.com/expressjs-blog-${branch}" -} - -get_log_file_path() { - echo "/srv/jasonpoage.com/logs/receive-${TIMESTAMP}.log" -} - -copy_env_file() { - local branch="$1" - local env_file - env_file="$(get_deploy_env_file "$branch")" - source "$env_file" - # Copy the .env file for the test environment - cp "$env_file" "$tmpdir/.env" || { - echo "Error: Failed to copy .env file for test environment." - return 1 - } -} - -clone_branch() { - local branch="$1" - local tmpdir="$2" - # This creates a proper working tree at $tmpdir, which is essential for submodules. - git clone --branch "$branch" "$GIT_DIR" "$tmpdir" || { - echo "Error: Failed to clone main application for tests to $tmpdir" - return 1 - } -} - -# Function to wait for a service to become available -_wait_for_service() { - local url="$1" - local timeout=30 # seconds - local start_time - start_time=$(date +%s) - echo "Waiting for service at $url to become available (timeout: ${timeout}s)..." - while :; do - if curl --silent --fail "$url" >/dev/null; then - echo "Service at $url is up!" - return 0 - fi - current_time=$(date +%s) - if ((current_time - start_time >= timeout)); then - echo "Error: Service at $url did not become available within ${timeout}s." - return 1 - fi - sleep 1 - done -} - -quick_deploy_expressjs_blog() { - local branch="$1" - local test_dir="$2" - local deploy_path="$3" - local envfile="$4" - echo "Moving tested deployment from '$test_dir' to '$deploy_path'..." - [[ -d "$deploy_path" ]] && rm -rf "$deploy_path" - mv "$test_dir" "$deploy_path" - git config -f /srv/jasonpoage.com/expressjs-blog.git/modules/content/config core.worktree "/srv/jasonpoage.com/expressjs-blog-testing/content" - ln -f "$envfile" "$deploy_path/.env" - - systemctl --user restart express-blog@"$branch".service -} - -# Smart dependency installation (only if package files changed) -get_dependencies() { - local oldrev="$1" - local newrev="$2" - local install_required=true - if [[ -n "$oldrev" && "$oldrev" != "0000000000000000000000000000000000000000" ]]; then - if ! git --git-dir="$GIT_DIR" diff-tree --name-only -r "$oldrev..$newrev" | grep -qE "(package\.json|yarn\.lock)$"; then - echo "No changes detected in package.json or yarn.lock. Skipping yarn install." - install_required=false - else - echo "Changes detected in package.json or yarn.lock. Running yarn install." - fi - else - echo "Running yarn install (initial deployment)." - fi - - if [[ "$install_required" == "true" ]]; then - yarn - fi -} - -# Single function to handle all repo and submodule updates (force-push safe) -update_repo_and_submodules() { - local branch="$1" - local deploy_path="$2" - - echo "Updating repository and submodules (force-push safe)..." - - cd "$deploy_path" || { - echo "Error: Could not change directory to $deploy_path" - return 1 - } - SAVED_GIT_DIR="$GIT_DIR" - unset GIT_DIR - - git remote show - git branch -l - # Update main repository - always fetch and hard reset (handles force pushes) - echo "Fetching and updating main repository..." - git fetch origin || { - echo "Error: Failed to fetch from origin" - return 1 - } - - git reset --hard "origin/$branch" || { - echo "Error: Failed to reset to origin/$branch" - return 1 - } - - # Update submodules - handle force pushes by resetting each submodule - echo "Updating submodules..." - git submodule update --init --recursive --force || { - echo "Standard submodule update failed, cleaning and retrying..." - return 1 - } - - echo "Repository and submodules updated successfully" - - export GIT_DIR="$SAVED_GIT_DIR" - unset SAVED_GIT_DIR -} - -# Updated conservative deployment function using the unified update -conservative_deploy_expressjs_blog() { - local branch="$1" - local oldrev="${2:-}" - local newrev="${3:-}" - local deploy_path="${4:-}" - local envfile="${5:-}" - - echo "Using preserving deployment method for production environment..." - - # set -x - - # Initialize the deployment directory if it doesn't exist - if [[ ! -d "$deploy_path" ]]; then - echo "Creating deployment directory..." - git clone --branch "$branch" "$GIT_DIR" "$deploy_path" || { - echo "Error: Failed to create deployment directory" - return 1 - } - fi - - # Single unified update function handles everything - update_repo_and_submodules "$branch" "$deploy_path" "$GIT_DIR" || return 1 - - cd "$deploy_path" || { - echo "Error: Could not change directory to $deploy_path" - return 1 - } - - ln -f "$envfile" "$deploy_path/.env" || return 1 - - get_dependencies "$oldrev" "$newrev" || return 1 - - yarn combine:css "$oldrev" "$newrev" "$GIT_DIR" || return 1 - - systemctl --user restart express-blog@"$branch".service - - # set +x -} - -deploy_expressjs_blog() { - local branch="$1" - local test_dir="$2" - local oldrev="${3:-}" - local newrev="${4:-}" - - # Define deployment path and env file path - local deploy_path envfile - deploy_path=$(get_deploy_path "$branch") - envfile=$(get_deploy_env_file "$branch") - - - case "$branch" in - "testing") - quick_deploy_expressjs_blog "$branch" "$test_dir" "$deploy_path" "$envfile" - return 0 - ;; - "staging") - # For production/main, use the preserving method (keep logs, update in place) - conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" - return 0 - ;; - "main") - conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" - # echo "Deployment available at: $deploy_path" - ;; - - esac -} - - -run_postreceive_tests() { - local branch="$1" - local tmpdir pidfile logfile - tmpdir="$2" - pidfile="$tmpdir/test.pid" - logfile=$(get_log_file_path) - # Trap to ensure cleanup even if script exits unexpectedly - trap "kill \"$(cat "$pidfile" 2>/dev/null)\" 2>/dev/null || true; rm -f \"$pidfile\"" EXIT - - echo "Running post-receive tests for branch '$branch' in temporary environment." - - clone_branch "$branch" "$tmpdir" || return 1 - - cd "$tmpdir" || { - echo "Error: Could not change directory to $tmpdir" - return 1 - } - - copy_env_file "$branch" || return 1 - - initialize_submodules "$tmpdir" || return 1 - - # echo "Skipping tests" - # return 0 - - echo "Running build scripts..." - yarn - combine_css || return 1 - - echo "Starting application for tests..." - systemctl --user stop express-blog@"$branch".service - nohup node src/app.js >>"$logfile" 2>&1 & - echo $! >"$pidfile" - - # set +x - wait_for_service "$logfile" - # set -x - - echo "Running tests..." - run_tests "$branch" "$pidfile" "$logfile" || return 1 - - kill "$(cat "$pidfile")" 2>/dev/null || true - - echo "Tests passed for branch '$branch' in temporary environment." - return 0 -} - -initialize_submodules() { - local worktree="$1" - echo "Initializing and updating submodules for test environment..." - git --git-dir="$worktree/.git" --work-tree="$worktree" submodule update --init --recursive || { - ls "$worktree" -a - echo "Error: Failed to initialize/update submodules for test environment." - return 1 - } -} -update_submodules() { - local worktree="$1" - git --work-tree="$worktree" submodule update --init --recursive || { - echo "Error: Failed to initialize/update submodules in deployment" - return 1 - } -} -wait_for_service() { - local logfile="$1" - # Wait for the application to become responsive - if ! _wait_for_service "$SERVER_SCHEMA://$SERVER_DOMAIN"; then - echo "Application did not start or respond for tests. Check logs in $logfile:" - cat "$logfile" # Display logs on failure - return 1 - fi -} - -run_tests() { - branch="$1" - pidfile="$2" - logfile="$3" - echo "Running yarn tests..." - if ! yarn test:postreceive; then - kill "$(cat "$pidfile")" 2>/dev/null || true - echo "Tests failed for branch '$branch'. Application logs from $logfile:" - cat "$logfile" - return 1 - fi -} - -combine_css() { - yarn --production=false combine:css || { - echo "Error: yarn combine:css failed." - return 1 - } -} - - -# 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 -} - -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 - - 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 -} +# Path: /path/to/gitbucket/data/repositories/USER/REPO.git/hooks/post-receive +set -x +while read oldrev newrev refname; do + # Call Jenkins + curl -X POST "https://jenkins.jasonpoage.com/generic-webhook-trigger/invoke" \ + --data-urlencode "branch=$refname" \ + --data-urlencode "oldrev=$oldrev" \ + --data-urlencode "newrev=$newrev" +done diff --git a/.githooks/post-receive.bak b/.githooks/post-receive.bak new file mode 100644 index 0000000..d252a2a --- /dev/null +++ b/.githooks/post-receive.bak @@ -0,0 +1,342 @@ +#!/bin/bash + +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 + +get_deploy_env_file() { + branch="$1" + echo "/srv/jasonpoage.com/${branch}.env" +} + +get_deploy_path() { + branch="$1" + echo "/srv/jasonpoage.com/expressjs-blog-${branch}" +} + +get_log_file_path() { + echo "/srv/jasonpoage.com/logs/receive-${TIMESTAMP}.log" +} + +copy_env_file() { + local branch="$1" + local env_file + env_file="$(get_deploy_env_file "$branch")" + source "$env_file" + # Copy the .env file for the test environment + cp "$env_file" "$tmpdir/.env" || { + echo "Error: Failed to copy .env file for test environment." + return 1 + } +} + +clone_branch() { + local branch="$1" + local tmpdir="$2" + # This creates a proper working tree at $tmpdir, which is essential for submodules. + git clone --branch "$branch" "$GIT_DIR" "$tmpdir" || { + echo "Error: Failed to clone main application for tests to $tmpdir" + return 1 + } +} + +# Function to wait for a service to become available +_wait_for_service() { + local url="$1" + local timeout=30 # seconds + local start_time + start_time=$(date +%s) + echo "Waiting for service at $url to become available (timeout: ${timeout}s)..." + while :; do + if curl --silent --fail "$url" >/dev/null; then + echo "Service at $url is up!" + return 0 + fi + current_time=$(date +%s) + if ((current_time - start_time >= timeout)); then + echo "Error: Service at $url did not become available within ${timeout}s." + return 1 + fi + sleep 1 + done +} + +quick_deploy_expressjs_blog() { + local branch="$1" + local test_dir="$2" + local deploy_path="$3" + local envfile="$4" + echo "Moving tested deployment from '$test_dir' to '$deploy_path'..." + [[ -d "$deploy_path" ]] && rm -rf "$deploy_path" + mv "$test_dir" "$deploy_path" + git config -f /srv/jasonpoage.com/expressjs-blog.git/modules/content/config core.worktree "/srv/jasonpoage.com/expressjs-blog-testing/content" + ln -f "$envfile" "$deploy_path/.env" + + systemctl --user restart express-blog@"$branch".service +} + +# Smart dependency installation (only if package files changed) +get_dependencies() { + local oldrev="$1" + local newrev="$2" + local install_required=true + if [[ -n "$oldrev" && "$oldrev" != "0000000000000000000000000000000000000000" ]]; then + if ! git --git-dir="$GIT_DIR" diff-tree --name-only -r "$oldrev..$newrev" | grep -qE "(package\.json|yarn\.lock)$"; then + echo "No changes detected in package.json or yarn.lock. Skipping yarn install." + install_required=false + else + echo "Changes detected in package.json or yarn.lock. Running yarn install." + fi + else + echo "Running yarn install (initial deployment)." + fi + + if [[ "$install_required" == "true" ]]; then + yarn + fi +} + +# Single function to handle all repo and submodule updates (force-push safe) +update_repo_and_submodules() { + local branch="$1" + local deploy_path="$2" + + echo "Updating repository and submodules (force-push safe)..." + + cd "$deploy_path" || { + echo "Error: Could not change directory to $deploy_path" + return 1 + } + SAVED_GIT_DIR="$GIT_DIR" + unset GIT_DIR + + git remote show + git branch -l + # Update main repository - always fetch and hard reset (handles force pushes) + echo "Fetching and updating main repository..." + git fetch origin || { + echo "Error: Failed to fetch from origin" + return 1 + } + + git reset --hard "origin/$branch" || { + echo "Error: Failed to reset to origin/$branch" + return 1 + } + + # Update submodules - handle force pushes by resetting each submodule + echo "Updating submodules..." + git submodule update --init --recursive --force || { + echo "Standard submodule update failed, cleaning and retrying..." + return 1 + } + + echo "Repository and submodules updated successfully" + + export GIT_DIR="$SAVED_GIT_DIR" + unset SAVED_GIT_DIR +} + +# Updated conservative deployment function using the unified update +conservative_deploy_expressjs_blog() { + local branch="$1" + local oldrev="${2:-}" + local newrev="${3:-}" + local deploy_path="${4:-}" + local envfile="${5:-}" + + echo "Using preserving deployment method for production environment..." + + # set -x + + # Initialize the deployment directory if it doesn't exist + if [[ ! -d "$deploy_path" ]]; then + echo "Creating deployment directory..." + git clone --branch "$branch" "$GIT_DIR" "$deploy_path" || { + echo "Error: Failed to create deployment directory" + return 1 + } + fi + + # Single unified update function handles everything + update_repo_and_submodules "$branch" "$deploy_path" "$GIT_DIR" || return 1 + + cd "$deploy_path" || { + echo "Error: Could not change directory to $deploy_path" + return 1 + } + + ln -f "$envfile" "$deploy_path/.env" || return 1 + + get_dependencies "$oldrev" "$newrev" || return 1 + + yarn combine:css "$oldrev" "$newrev" "$GIT_DIR" || return 1 + + systemctl --user restart express-blog@"$branch".service + + # set +x +} + +deploy_expressjs_blog() { + local branch="$1" + local test_dir="$2" + local oldrev="${3:-}" + local newrev="${4:-}" + + # Define deployment path and env file path + local deploy_path envfile + deploy_path=$(get_deploy_path "$branch") + envfile=$(get_deploy_env_file "$branch") + + + case "$branch" in + "testing") + quick_deploy_expressjs_blog "$branch" "$test_dir" "$deploy_path" "$envfile" + return 0 + ;; + "staging") + # For production/main, use the preserving method (keep logs, update in place) + conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" + return 0 + ;; + "main") + conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" + # echo "Deployment available at: $deploy_path" + ;; + + esac +} + + +run_postreceive_tests() { + local branch="$1" + local tmpdir pidfile logfile + tmpdir="$2" + pidfile="$tmpdir/test.pid" + logfile=$(get_log_file_path) + # Trap to ensure cleanup even if script exits unexpectedly + trap "kill \"$(cat "$pidfile" 2>/dev/null)\" 2>/dev/null || true; rm -f \"$pidfile\"" EXIT + + echo "Running post-receive tests for branch '$branch' in temporary environment." + + clone_branch "$branch" "$tmpdir" || return 1 + + cd "$tmpdir" || { + echo "Error: Could not change directory to $tmpdir" + return 1 + } + + copy_env_file "$branch" || return 1 + + initialize_submodules "$tmpdir" || return 1 + + # echo "Skipping tests" + # return 0 + + echo "Running build scripts..." + yarn + combine_css || return 1 + + echo "Starting application for tests..." + systemctl --user stop express-blog@"$branch".service + nohup node src/app.js >>"$logfile" 2>&1 & + echo $! >"$pidfile" + + # set +x + wait_for_service "$logfile" + # set -x + + echo "Running tests..." + run_tests "$branch" "$pidfile" "$logfile" || return 1 + + kill "$(cat "$pidfile")" 2>/dev/null || true + + echo "Tests passed for branch '$branch' in temporary environment." + return 0 +} + +initialize_submodules() { + local worktree="$1" + echo "Initializing and updating submodules for test environment..." + git --git-dir="$worktree/.git" --work-tree="$worktree" submodule update --init --recursive || { + ls "$worktree" -a + echo "Error: Failed to initialize/update submodules for test environment." + return 1 + } +} +update_submodules() { + local worktree="$1" + git --work-tree="$worktree" submodule update --init --recursive || { + echo "Error: Failed to initialize/update submodules in deployment" + return 1 + } +} +wait_for_service() { + local logfile="$1" + # Wait for the application to become responsive + if ! _wait_for_service "$SERVER_SCHEMA://$SERVER_DOMAIN"; then + echo "Application did not start or respond for tests. Check logs in $logfile:" + cat "$logfile" # Display logs on failure + return 1 + fi +} + +run_tests() { + branch="$1" + pidfile="$2" + logfile="$3" + echo "Running yarn tests..." + if ! yarn test:postreceive; then + kill "$(cat "$pidfile")" 2>/dev/null || true + echo "Tests failed for branch '$branch'. Application logs from $logfile:" + cat "$logfile" + return 1 + fi +} + +combine_css() { + yarn --production=false combine:css || { + echo "Error: yarn combine:css failed." + return 1 + } +} + + +# 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 +} + +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 + + 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 584cf79..295900b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -61,8 +61,7 @@ echo "params.newrev: '${params.newrev}'" echo "Old revision: ${env.OLD_REV}" echo "New revision: ${env.NEW_REV}" - sh "mkdir -p '${env.LOG_DIR}/server' '{env.LOG_DIR}/test-results'" - + sh "mkdir -p '${env.LOG_DIR}/server' '${env.LOG_DIR}/test-results'" } } } @@ -139,7 +138,8 @@ dir(BUILD_DIR) { sh """ sudo systemctl stop ${env.SERVICE_NAME} || true - nohup node src/app.js >> '${env.SERVER_LOG_FILE}' 2>&1 & + corepack enable + nohup yarn run prod >> '${env.SERVER_LOG_FILE}' 2>&1 & echo \$! > '${env.PIDFILE}' """ } diff --git a/package.json b/package.json index 2167786..42d4b0f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "combine:css": "node scripts/combine-css.js", "test": "mocha --reporter spec \"test/**/*.unit.test.js\" \"test/**/*.property.test.js\"", "start": "nodemon ./src/app.js --trace-exit", + "prod": "node ./src/app.js --trace-exit", "debug": "nodemon --inspect-brk ./src/app.js --trace-exit", "maildev": "maildev", "main": "pm2 start ecosystem.config.js --only expressjs-blog-main", @@ -64,14 +65,14 @@ "xss-clean": "^0.1.4" }, "devDependencies": { - "@faker-js/faker": "^9.8.0", + "@faker-js/faker": "^9.9.0", "chai": "^5.2.1", "chai-as-promised": "^8.0.1", "fast-check": "^4.2.0", "mocha": "^11.7.1", "mock-fs": "^5.5.0", "node-fetch": "^3.3.2", - "pm2": "^6.0.6", + "pm2": "^6.0.8", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "proxyquire": "^2.1.3", diff --git a/yarn.lock b/yarn.lock index 583ece5..cecf121 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,7 +50,7 @@ languageName: node linkType: hard -"@faker-js/faker@npm:^9.8.0": +"@faker-js/faker@npm:^9.9.0": version: 9.9.0 resolution: "@faker-js/faker@npm:9.9.0" checksum: 10c0/02107cb6915217b4831fa4b261603165972eb7f09d2ab2e1d4b75807df2d425a2bac8621cddbfdcc158c2c447c2bce2e14939d51c4a63fbd832df55452e7f73c @@ -2067,7 +2067,7 @@ version: 0.0.0-use.local resolution: "express-blog@workspace:." dependencies: - "@faker-js/faker": "npm:^9.8.0" + "@faker-js/faker": "npm:^9.9.0" better-sqlite3: "npm:^12.2.0" body-parser: "npm:^2.2.0" chai: "npm:^5.2.1" @@ -2098,7 +2098,7 @@ nodemailer: "npm:^7.0.5" nodemon: "npm:^3.1.10" path: "npm:^0.12.7" - pm2: "npm:^6.0.6" + pm2: "npm:^6.0.8" postcss: "npm:^8.5.6" postcss-import: "npm:^16.1.1" proxyquire: "npm:^2.1.3" @@ -4681,7 +4681,7 @@ languageName: node linkType: hard -"pm2@npm:^6.0.6": +"pm2@npm:^6.0.8": version: 6.0.8 resolution: "pm2@npm:6.0.8" dependencies: