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
65 changes: 48 additions & 17 deletions .github/workflows/ci.yml → .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI
name: CI/CD

on:
pull_request:
Expand All @@ -22,9 +22,8 @@ env:
SERVICE: ufound-it-backend

jobs:
# Deploying is part of this job, not a separate one, because a new job
# starts with a fresh checkout and would have to build again. The job
# keeps its name since branch protection requires a check named "checks".
# 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:
Expand All @@ -43,15 +42,33 @@ jobs:
- run: npm test
- run: npm run build

# Only pushes to main deploy. Pull requests stop after the checks.
# 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
if: github.event_name == 'push'
run: |
find "$WEB_DIR" -mindepth 1 -delete
cp -r frontend/dist/. "$WEB_DIR"/
- name: Deploy backend
if: github.event_name == 'push'
run: |
# npm ci needs the workspace layout, so both package.json files are
# copied even though only the backend runs here. backend/.env is
Expand All @@ -73,19 +90,33 @@ jobs:
sudo -n systemctl restart "$SERVICE"
- name: Check the deployed site
if: github.event_name == 'push'
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 curl -fsS http://localhost/project2/graphql \
-H 'content-type: application/json' \
--data '{"query":"{ health }"}' | grep -q '"ok"'; then
curl -fsS -o /dev/null http://localhost/project2/
echo "Deployed and responding."
exit 0
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
echo "The backend did not answer through Apache." >&2
sudo -n systemctl status "$SERVICE" --no-pager || true
exit 1
# 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."
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Never:
- **VM address:** http://it2810-25.idi.ntnu.no/project2/
- **Backend port:** 3001
- **GraphQL endpoint:** The frontend calls `/project2/graphql` on its own origin. Apache must proxy it to `http://localhost:3001/graphql`, like the Vite dev proxy does locally. `VITE_GRAPHQL_URL` overrides the URL at build time if needed.
- **How to deploy:** Automatic. CI deploys every push to `main` that passes the checks. The frontend goes to `/var/www/html/project2`, the backend runs from `/srv/ufound-it` as the systemd service `ufound-it-backend`. One-time VM setup is in `docs/ci-cd.md`.
- **How to deploy:** Automatic. The `deploy` job in CI deploys every push to `main` that passes `checks`, and GitHub tracks it as the `production` environment. The frontend goes to `/var/www/html/project2`, the backend runs from `/srv/ufound-it` as the systemd service `ufound-it-backend`. One-time VM setup is in `docs/ci-cd.md`.

## AI usage documentation

Expand Down
28 changes: 18 additions & 10 deletions docs/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This page explains how our continuous integration (CI) and deployment (CD) work.

## Continuous integration

The workflow is defined in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml).
The workflow is defined in [`.github/workflows/ci-cd.yml`](../.github/workflows/ci-cd.yml).

### When it runs

Expand Down Expand Up @@ -73,16 +73,20 @@ run, and also dismisses existing approvals.

## Continuous deployment

After the checks pass on a push to `main`, the same job deploys to the VM.
Pull requests stop after the checks. It is the same job because a new job
would start from a fresh checkout and have to build again, and the job keeps
the name `checks` because branch protection requires a check with that name.
After `checks` passes on a push to `main`, a second job, `deploy`, deploys to
the VM. Pull requests only run `checks`. The deploy job starts from a fresh
checkout, so it installs and builds again before deploying.

| Step | What it does |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Deploy frontend | Replaces the contents of `/var/www/html/project2` with `frontend/dist`. Apache serves it at `/project2`. |
| Deploy backend | Copies `backend/dist` to `/srv/ufound-it`, installs the backend's production dependencies there, copies the Node binary CI used, and restarts the service. |
| Check the deployed site | Calls the `health` query through Apache and loads the start page. The run fails if the backend does not answer within about 20 seconds. |
`deploy` belongs to the GitHub environment `production`. GitHub lists every
deploy under _Deployments_ on the repository page, with the commit that is live
and a link to the site.

| Step | What it does |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm ci`, `npm run build` | Installs and builds again, because the job starts from a fresh checkout. |
| Deploy frontend | Replaces the contents of `/var/www/html/project2` with `frontend/dist`. Apache serves it at `/project2`. |
| Deploy backend | Copies `backend/dist` to `/srv/ufound-it`, installs the backend's production dependencies there, copies the Node binary CI used, and restarts the service. |
| Check the deployed site | Calls `health` through Apache, then a real `sightings` query, then loads the start page. `health` does not touch the database, so only the second query catches a database that cannot be reached. |

The backend runs from `/srv/ufound-it` and not from the runner's work folder,
because the next CI run, including one for a pull request, wipes that folder.
Expand Down Expand Up @@ -147,5 +151,9 @@ sudoers files if it does not.
- **The site check fails.** Read the service status at the end of the log, or
run `journalctl -u ufound-it-backend -n 50` on the VM. A wrong or missing
`.env` is the usual cause.
- **`Access denied for user 'project_2_app'`** means the password in
`/srv/ufound-it/backend/.env` does not match the one MySQL has for that user.
Fix the file, then run `sudo systemctl restart ufound-it-backend`. The backend
only reads `.env` when it starts.
- **Rolling back.** Revert the merge commit on `main` with a pull request. The
revert is deployed like any other merge.