Files
nick-doc/08 - Operations/CI-CD Pipeline.md
2026-05-23 20:35:34 +03:30

260 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: CI-CD Pipeline
tags: [operations]
---
# CI/CD Pipeline
How code goes from a push to a running container in production. The CI is **Gitea Actions** running on the same Gitea instance that hosts the repos. The CD is **Watchtower** on the production host (covered in [[Deployment]]).
---
## 1. Where workflows live
| Repo | Path | Files |
|------|------|-------|
| Backend | `.gitea/workflows/` | `docker-build-simple.yml`, `docker-build-dev.yml`, `docker-build-no-cache.yml` |
| Frontend | `.gitea/workflows/` | `deploy.yml`, `devDeploy.yml` |
Gitea Actions speaks the same YAML dialect as GitHub Actions — most third-party actions (`actions/checkout@v4`, `docker/login-action@v3`, `docker/build-push-action@v5`) work unchanged.
---
## 2. Required secrets
Configured per repo at **Settings → Actions → Secrets**.
| Secret | Repo | Purpose |
|--------|------|---------|
| `GITEATOKEN` | both | Personal access token for the `manawenuz` user with `write:packages` scope. Used by every workflow to log into the container registry at `git.manko.yoga`. |
| `SENTRY_AUTH_TOKEN` | frontend | (Optional) For source-map upload during Next.js build. Skipped if absent. |
The registry itself is implicit: `git.manko.yoga` with `manawenuz` as the user. Image paths are `git.manko.yoga/manawenuz/<image>`.
> [!warning] If `GITEATOKEN` expires or is rotated, all workflows fail at the `docker/login-action` step. Rotate proactively (annual reminder).
---
## 3. Backend workflows
### `docker-build-simple.yml` — manual build
```yaml
name: Manual Build and Push Docker Image
on:
workflow_dispatch:
inputs:
version:
description: 'Version to build (leave empty for package.json)'
required: false
type: string
```
- **Trigger.** Manual only (via Gitea UI → Actions → "Run workflow").
- **Steps.** Checkout → buildx → `docker login` → read version (input or `package.json`) → build `Dockerfile.prod` → push tags `:<version>` and `:dev` → echo result.
- **When to use.** Cutting an ad-hoc build of a specific commit without merging to a branch. The `:dev` tag is overwritten — production (`:latest`) is **not** touched.
- **Cache.** Uses `type=gha` cache to speed up subsequent runs.
### `docker-build-dev.yml` — dev branch auto-build
```yaml
on:
push:
branches: [ development ]
tags: [ 'v*' ]
```
- **Trigger.** Every push to `development` and every tag matching `v*`.
- **Tags pushed.** `:dev-<package-version>` + moving `:dev`.
- **Effect.** Refreshes the dev image. The production Watchtower **does not** watch `:dev`, so this is safe to push as often as you want.
### `docker-build-no-cache.yml` — production build
```yaml
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
```
- **Trigger.** Every push to `main` (or `master`) and every `v*` tag.
- **Tags pushed.** `:<package-version>` + moving `:latest`.
- **Effect.** Watchtower polls `:latest`, detects the new digest, restarts `nickapp-backend` on the production host. See [[Deployment#routine-deploy]].
- **No cache.** The file is named "No Cache" but actually does not pass `cache-from`/`cache-to`, so each build is from scratch. Slower (~58 min) but eliminates a class of stale-layer bugs. The `simple` workflow uses GHA cache for speed.
> [!tip] If you need to invalidate a cached layer in the `simple` workflow, run `no-cache` once — the resulting tag overwrites the registry digest and `simple`'s next run will start from a cleaner base.
---
## 4. Frontend workflows
Both workflows share the same shape: spin up a `node:22` container, run a deploy shell script that does `docker login + build + push`.
### `deploy.yml` — production
```yaml
on:
push:
branches: [ main, master ]
workflow_dispatch:
```
Calls `./scripts/deploy.sh` — see [[Scripts#deployment]]. The script:
1. Reads `package.json` version.
2. `docker login git.manko.yoga -u manawenuz -p $GITEATOKEN`.
3. Builds `git.manko.yoga/manawenuz/escrow-frontend:<version>` and `:latest` from `Dockerfile`.
4. Pushes both tags.
`:latest` is what production Watchtower watches → live deploy follows automatically.
### `devDeploy.yml` — development branch
Same as `deploy.yml` but triggered on `development` and runs `./scripts/deployDev.sh`, which pushes only `:dev`.
---
## 5. End-to-end timeline (production deploy)
```
t=0 Developer merges PR → main
t+5s Gitea webhook fires
t+10s Gitea Actions runner pulls repo, starts container
t+30s docker/setup-buildx-action initialised
t+45s docker/login-action authenticated
t+2-5m docker/build-push-action builds Dockerfile.prod
t+5m Push to git.manko.yoga/manawenuz/escrow-backend:latest
t+5m+ Watchtower (next poll, up to 5 min) detects new digest
t+10m Watchtower stops old container, starts new one
t+10m40s start_period=40s elapses, healthcheck passes
t+11m Nginx routes traffic to the new container
```
**Typical SLA: 1012 minutes from merge to live.** For an emergency rollback see [[Deployment#roll-back]].
---
## 6. Versioning automation
Tied to `backend/scripts/auto-version.sh` + `ai-enhanced.sh` (and the frontend mirror). Full reference in [[Git Workflow#versioning]] and [[Scripts#auto-version-sh]].
In short:
```bash
# Developer side, on the branch they're releasing:
npm run smart-release
# → AI analyses last commit, picks bump (major/minor/patch/skip)
# → bumps package.json
# → commits "chore: bump version to vX.Y.Z"
# → tags vX.Y.Z
# → git push && git push --tags
```
The push to `main` (or the `v*` tag) then triggers `docker-build-no-cache.yml`, which:
- Reads the new version from `package.json` (`node -p "require('./package.json').version"`)
- Builds and pushes `:<version>` + `:latest`
So both the **image tag** and the **git tag** carry the same `vX.Y.Z` — easy to correlate when investigating an issue.
---
## 7. Adding tests to the pipeline
The workflows today only build + push; they do **not** run Jest or Playwright. To gate releases on tests, add a `test` job before the build:
```yaml
jobs:
test:
runs-on: ubuntu-latest
container: node:22
steps:
- uses: actions/checkout@v4
- run: yarn install --frozen-lockfile
- run: yarn lint
- run: yarn test --ci --runInBand
- run: yarn test:e2e # if a service container is available
build-and-push:
needs: test
runs-on: ubuntu-latest
# ...existing steps...
```
Or run lint + typecheck as a pre-gate using a separate workflow that triggers on PR opened/synchronised.
---
## 8. Inspecting a build
In Gitea: **Actions → workflow → run** to see real-time logs.
Useful CLI for the registry from your laptop:
```bash
# List images and tags
curl -s -u "manawenuz:$GITEATOKEN" \
"https://git.manko.yoga/v2/manawenuz/escrow-backend/tags/list" | jq
# Pull a specific tag
docker login git.manko.yoga -u manawenuz
docker pull git.manko.yoga/manawenuz/escrow-backend:2.6.3
```
---
## 9. Self-hosted runner notes
Gitea Actions can use either built-in `act_runner` or your own. Currently the workflows are written for `runs-on: ubuntu-latest`, which the act_runner supplies via a generic Ubuntu container. If you need:
- More CPU/RAM for builds → register a beefier self-hosted runner and change `runs-on:` to its label.
- A Docker-in-Docker setup (frontend `deploy.yml` does this with `options: --privileged`) — confirm the runner trusts the workflow.
---
## 10. Failure modes & remediation
| Failure | Most likely cause | Fix |
|---------|------------------|-----|
| `unauthorized: authentication required` at push | `GITEATOKEN` expired or lacks `write:packages` | Rotate the token, update the repo secret |
| `Cannot perform an interactive login from a non TTY device` | Old docker-login-action version | Bump to `docker/login-action@v3` |
| Build hangs at `yarn install` | npm registry timeout | Increase `network-timeout` (already 600000); re-run |
| Image pushed but Watchtower doesn't roll | Watchtower can't reach the registry | `docker logs watchtower`; verify `/root/.docker/config.json` is mounted into the container |
| New container fails healthcheck | App crash on boot | `docker logs nickapp-backend`; check env vars, follow [[Incident Response]] |
| Multi-arch warnings about platform | Build runner is arm64 but prod is amd64 | Add `--platform=linux/amd64` to `docker/build-push-action` inputs |
| Image size grew suddenly | Dev dep crept into prod stage | Audit `Dockerfile.prod` for missing `--production` flag in the runtime stage |
---
## 11. Pipeline diagram
```
Push to development Push to main
│ │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ docker-build-dev.yml │ │ docker-build-no-cache.yml │
│ (backend) │ │ (backend) │
│ devDeploy.yml (frontend) │ │ deploy.yml (frontend) │
└───────────────┬───────────┘ └───────────────┬───────────┘
│ │
push :<version>,:dev push :<version>,:latest
│ │
▼ ▼
git.manko.yoga/manawenuz/...:dev git.manko.yoga/manawenuz/...:latest
│ │
│ ▼
│ ┌──────────────────────────┐
│ │ Watchtower │
│ │ (poll every 5 minutes) │
│ └──────────────┬───────────┘
│ │
manual pull on staging restart containers
Production live
```
Cross-links: [[Deployment]] for what happens on the host, [[Git Workflow]] for what happens upstream, [[Scripts]] for the deploy shell scripts.