diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-cd.yml similarity index 60% rename from .github/workflows/ci.yml rename to .github/workflows/ci-cd.yml index 58ad237..8c20505 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-cd.yml @@ -1,4 +1,4 @@ -name: CI +name: CI/CD on: pull_request: @@ -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: @@ -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 @@ -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." diff --git a/AGENTS.md b/AGENTS.md index 339a4d8..900952b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/docs/ci-cd.md b/docs/ci-cd.md index b1c3dd0..c51d575 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -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 @@ -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. @@ -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.