1.The pipeline, and the gates in the right order
A pipeline is a fixed sequence that runs on every push: lint, type check, unit tests, integration tests, security scan, build, deploy, migrate, health check, smoke test. **GitHub Actions** and **GitLab CI** both describe it as YAML in your repository — jobs made of steps, running on a fresh machine each time. A **test environment** is what those tests run against: a real PostgreSQL and Redis started for the job, not mocks and not production.
Key features
- The order is cheapest-first: lint → types → unit → integration → scan → build
- Build once; every later stage acts on the artefact, never on the source again
- Each gate names a different class of failure — that is what makes it a pipeline
- Test against real PostgreSQL and Redis service containers, not SQLite and not mocks
- `makemigrations --check --dry-run` catches the model change with no migration
- `check --deploy` must run against the *production* settings module to mean anything
The roadmap's pipeline, with what each stage actually catches
GitHub Actions and GitLab CI — the same shape, two vocabularies
- Whole: jobs: test: runs-on: ubuntu-latest permissions: { contents: read } services: db: { image: postgres:16 } steps: - uses: actions/checkout@v4 - run: python -m pytest -q
- jobs: — Jobs run in parallel by default: Independent jobs start together; `needs:` is what imposes order. Splitting lint and tests into separate jobs gives you both results at once instead of stopping at the first.
- runs-on: ubuntu-latest — A fresh machine, every run: Nothing survives from the last run unless you cache it deliberately. That is what makes a green pipeline meaningful — and why "works on my machine" cannot happen here.
- permissions: { contents: read } — The token this job gets: Narrow it explicitly. A job that only runs tests has no reason to be able to write to the repository, and a compromised dependency inherits exactly these permissions.
- services: — Real backing services: A PostgreSQL container started for this job. Django creates and destroys its test database inside it, so constraints, transactions and migrations run against the engine production uses.
- actions/checkout@v4 — A pinned third-party step: A version, not a moving branch. An action runs arbitrary code in a context that holds your secrets, so what it resolves to should not change between runs without you choosing it.
- python -m pytest -q — The step that is just a command: It runs the same thing you run locally. When the pipeline gets clever here — bespoke flags, CI-only paths — a green pipeline stops predicting a green machine.
Command
python manage.py makemigrations --check --dry-run # fails if a migration is missingRun the gates cheapest-first — lint, types, unit, integration, security scan — so a two-second failure never waits behind a five-minute one, and build once so everything afterwards acts on the artefact. Both GitHub Actions and GitLab CI are the same shape: YAML in the repository, jobs of steps, a fresh machine each run. Test against a real PostgreSQL service container, because SQLite is green exactly when the schema is wrong. Install from a lock file so a red build means your change. And add the two Django gates a test suite does not give you: `makemigrations --check --dry-run`, and `check --deploy` run against the production settings module.