Skip to content

Docs: Document how to roll back a bad deploy #4

Docs: Document how to roll back a bad deploy

Docs: Document how to roll back a bad deploy #4

Workflow file for this run

name: CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
# Cancel outdated pull request runs to save runner time and energy. Runs on
# main are never cancelled, so a deploy is not stopped halfway; a newer
# merge waits for the running one instead.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Apache serves this folder at /project2 (see docs/ci-cd.md).
WEB_DIR: /var/www/html/project2
# The backend runs from here, outside the runner's work folder, which is
# wiped by the next checkout.
APP_DIR: /srv/ufound-it
SERVICE: ufound-it-backend
jobs:
# The job keeps its name since branch protection requires a check named
# "checks". Pull requests only run this job.
checks:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
# npm ci installs exactly what package-lock.json says, so CI matches
# what we run locally.
- run: npm ci
- run: npm run lint
- run: npm run format:check
- run: npm test
- run: npm run build
# A separate job so it can belong to the "production" environment, which
# makes GitHub list each deploy, which commit is live, and the site URL.
# A new job starts from a fresh checkout, so it installs and builds again.
deploy:
needs: checks
if: github.event_name == 'push'
runs-on: self-hosted
environment:
name: production
url: http://it2810-25.idi.ntnu.no/project2/
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Deploy frontend
run: |
find "$WEB_DIR" -mindepth 1 -delete
cp -r frontend/dist/. "$WEB_DIR"/
- name: Deploy backend
run: |
# npm ci needs the workspace layout, so both package.json files are
# copied even though only the backend runs here. backend/.env is
# created once by hand on the VM and is left alone.
mkdir -p "$APP_DIR/backend" "$APP_DIR/frontend"
cp package.json package-lock.json "$APP_DIR"/
cp backend/package.json "$APP_DIR/backend/"
cp frontend/package.json "$APP_DIR/frontend/"
rm -rf "$APP_DIR/backend/dist"
cp -r backend/dist "$APP_DIR/backend/"
(cd "$APP_DIR" && npm ci --omit=dev --workspace backend)
# Node is only installed in the runner's tool cache, so the service
# gets its own copy of the same version CI tested with. Renaming
# over the old binary works while the service is still running it.
cp "$(command -v node)" "$APP_DIR/node.new"
mv -f "$APP_DIR/node.new" "$APP_DIR/node"
sudo -n systemctl restart "$SERVICE"
- name: Check the deployed site
run: |
graphql() {
curl -fsS http://localhost/project2/graphql \
-H 'content-type: application/json' \
--data "{\"query\":\"$1\"}"
}
# The backend needs a moment to start, so retry before failing.
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if graphql '{ health }' | grep -q '"ok"'; then
break
fi
if [ "$attempt" = 10 ]; then
echo "The backend did not answer through Apache." >&2
sudo -n systemctl status "$SERVICE" --no-pager || true
exit 1
fi
sleep 2
done
# health does not touch the database, so a wrong password in .env
# or a stopped MySQL only shows up in a real query.
result=$(graphql '{ sightings(first: 1) { totalCount } }')
if ! echo "$result" | grep -q '"totalCount"'; then
echo "The backend is up but cannot read sightings: $result" >&2
exit 1
fi
curl -fsS -o /dev/null http://localhost/project2/
echo "Deployed and responding."