Continuous integration of Cluster ERP is one GitHub Actions workflow, .github/workflows/ci.yml. It runs three gate jobs on every push and pull request and adds a deploy job for version tags. This page explains each job, the order between them, and the conventions for tags and CHANGELOG.md. How a tag then moves through staging is in environments and promotion.
In short¶
- Triggers: a push to any branch, a push of a tag that matches
v*, and every pull request (.github/workflows/ci.yml:25-29). - Three gate jobs run in parallel:
lint,lock, andtest(.github/workflows/ci.yml:37-124). - The
deployjob runs only forrefs/tags/v*and only after all three gates pass (.github/workflows/ci.yml:126-129). - The workflow token has read access to repository contents only (
.github/workflows/ci.yml:31-32). - Every job installs Python packages from
requirements.lockwith--require-hashes(.github/workflows/ci.yml:47,.github/workflows/ci.yml:106).
How it works¶
The job graph:
flowchart LR push["push or pull request"] --> lint["lint: ruff + mypy"] push --> lock["lock: lock integrity + advisories"] push --> test["test: migrations + pytest"] lint --> deploy["deploy: tag v* only"] lock --> deploy test --> deploy deploy --> node["install.sh --role production"]
In prose: the three gates start together on every event; the deploy job waits for all three and runs only for a version tag; it then calls the installer on the production node.
Lint job¶
Installs the locked packages, the project, the dev extra, and PySide6==6.8.3, then runs ruff check src tests and mypy src (.github/workflows/ci.yml:38-52). The desktop toolkit is installed because mypy src also checks the Qt client (.github/workflows/ci.yml:48-49).
Lock job¶
uv lock --checkfails whenuv.lockdoes not matchpyproject.toml(.github/workflows/ci.yml:59-62).- The job exports
requirements.lockagain with the same command and fails on any difference (.github/workflows/ci.yml:63-68). pip-auditchecks every pin inrequirements.lockin strict mode; a pin with a known advisory fails the job (.github/workflows/ci.yml:72-74).
The pins in [tool.uv] are the versions that the full battery and pytest last ran on; the comment describes how to move one (pyproject.toml:155-166).
Test job¶
The job starts a postgres:16 service container and passes database settings and a CI-only secret through the job environment (.github/workflows/ci.yml:79-99). The values are omitted under the security disclosure policy. It then:
- applies the whole Alembic history to the empty database (
.github/workflows/ci.yml:110-113); - fails unless there is exactly one Alembic head (
.github/workflows/ci.yml:114-117); - runs
pytest(.github/workflows/ci.yml:124).
CLUSTER_SKIP_LIVE_TESTS=1 skips the tests that need a running server, so the waiver is visible (.github/workflows/ci.yml:17-20, .github/workflows/ci.yml:99).
Deploy job¶
The job uses the GitHub environment production, and the concurrency group production-deploy never cancels a running deploy (.github/workflows/ci.yml:131-136). It requires four secrets and one variable, by name DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY, DEPLOY_HOST_KEY, and DEPLOY_DOMAIN, and stops when one is empty (.github/workflows/ci.yml:139-150). The node's host key comes from a secret and is checked strictly (.github/workflows/ci.yml:153-155). The job runs install.sh --tag <tag> --domain <domain> --role production on the node (.github/workflows/ci.yml:155-156), so the promotion gate of the installer applies (deploy/install.sh:101-107).
Why it is built this way¶
- The migration step runs on Linux PostgreSQL because it is "the first place a Windows-authored migration meets Linux PostgreSQL" (
.github/workflows/ci.yml:110-112). - "a drifted lock means somebody edited one without the other" (
.github/workflows/ci.yml:60-61). - "a pin with a known advisory does not ship" (
.github/workflows/ci.yml:73). - The deploy job needs an approval in the
productionenvironment so that "a tag alone never reaches a customer node" (.github/workflows/ci.yml:7-12).
Rules and boundaries¶
- Release tags follow
v<version>; the promotion record followsstaging-ok/v<version>(.github/workflows/ci.yml:28,deploy/install.sh:15-21). CHANGELOG.mdfollows Keep a Changelog and Semantic Versioning; it opens with an[Unreleased]section, and each release has a heading[<version>] - <date>with groups such as Added, Changed, and Fixed (CHANGELOG.md:1-14).- "Every production deployment names the tag it ships; a tag is never moved." (
CHANGELOG.md:8). ruffandmypyversions are pinned in thedevextra so that CI judges with the same tools as the workstation (pyproject.toml:75-84). See lint and type check.
Interaction with other parts¶
- The browser battery is not part of the workflow; it runs on the staging node before the promotion tag is set (
.github/workflows/ci.yml:21-22). See E2E battery. - The Python tests are described in Python tests.
Limits and open points¶
alembic checkis not a gate; the comment explains that the model metadata inmigrations/env.pyis incomplete (.github/workflows/ci.yml:118-123).- Whether the
productionenvironment really has required reviewers is a repository setting: Not determined from code. Only the header comment of the workflow was examined (.github/workflows/ci.yml:7-12).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| lock integrity gate | lock |
The CI job that checks the lock files against pyproject.toml and checks the pins for advisories. |
kilit bütünlüğü kapısı | .github/workflows/ci.yml:54-74 |
| production environment | environment: production |
The GitHub environment the deploy job is bound to, where a person approves each deploy. | üretim ortamı | .github/workflows/ci.yml:131-133 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.