Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ jobs:

# The script is never copied to the server: bash reads it from stdin.
# github.sha is exactly the commit the checks above have tested.
- name: Deploy to server (dry run)
- name: Deploy to server
run: |
ssh -i ~/.ssh/deploy_key \
"${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
Expand Down
47 changes: 41 additions & 6 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
# Deploys one commit of the Coderr backend. Runs on the server and is fed
# through ssh by the deploy job in .github/workflows/ci.yml:
# ssh <host> "bash -s -- <commit-sha>" < deploy/deploy.sh
#
# First version: dry run only. It reports what a deployment would change
# and takes a database backup, but touches neither code nor database.
set -euo pipefail

APP_DIR=/var/www/coderr/backend
BACKUP_DIR="$HOME/backups/coderr"
KEEP_BACKUPS=10
SERVICE=gunicorn-coderr

sha="${1:-}"
if [[ ! "$sha" =~ ^[0-9a-f]{40}$ ]]; then
Expand Down Expand Up @@ -50,6 +48,34 @@ backup_database() {
| xargs -r rm --
}

# HUP makes the Gunicorn master start fresh workers with the new code while
# the old ones finish their requests. Gunicorn runs as this user, so no
# sudo is needed.
reload_gunicorn() {
local pid old_workers new_workers common
pid="$(systemctl show -p MainPID --value "$SERVICE")"
if [ "$pid" = "0" ]; then
echo "$SERVICE is not running." >&2
exit 1
fi
old_workers="$(pgrep -P "$pid" | sort)" || true
kill -HUP "$pid"

# Gunicorn's graceful timeout is 30 seconds, so 40 seconds also covers
# an old worker that is still busy with a slow request.
for _ in $(seq 1 40); do
sleep 1
new_workers="$(pgrep -P "$pid" | sort)" || true
common="$(comm -12 <(echo "$old_workers") <(echo "$new_workers"))"
if [ -n "$new_workers" ] && [ -z "$common" ]; then
echo "Gunicorn reloaded, workers: $(echo $new_workers)"
return 0
fi
done
echo "Gunicorn workers were not replaced within 40 seconds." >&2
exit 1
}

cd "$APP_DIR"

# Tracked files edited by hand on the server would get mixed with the new
Expand All @@ -76,8 +102,17 @@ echo "--- New migration files ---"
git diff --name-only --diff-filter=A HEAD "$sha" -- '*/migrations/*.py'
echo "--- Changes to requirements.txt ---"
git diff HEAD "$sha" -- requirements.txt
echo "--- Unapplied migrations of the running code ---"
.venv/bin/python manage.py migrate --plan

backup_database
echo "Dry run finished, nothing was deployed."

git merge --ff-only --quiet "$sha"
echo "Code is now at $(git log --oneline -1 HEAD)"

# These steps run on every deployment, changed or not. A deployment that
# failed halfway is then completed by simply running the job again.
.venv/bin/pip install --quiet --disable-pip-version-check -r requirements.txt
.venv/bin/python manage.py check
.venv/bin/python manage.py migrate --noinput
.venv/bin/python manage.py collectstatic --noinput
reload_gunicorn
echo "Deployment finished."