Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Operations

Explanation

CI pipeline and release

The jobs of the CI workflow, the lock and advisory gate, the gated production deploy job, and the tag and changelog conventions of a release.

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, and test (.github/workflows/ci.yml:37-124).
  • The deploy job runs only for refs/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.lock with --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

  1. uv lock --check fails when uv.lock does not match pyproject.toml (.github/workflows/ci.yml:59-62).
  2. The job exports requirements.lock again with the same command and fails on any difference (.github/workflows/ci.yml:63-68).
  3. pip-audit checks every pin in requirements.lock in 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:

  1. applies the whole Alembic history to the empty database (.github/workflows/ci.yml:110-113);
  2. fails unless there is exactly one Alembic head (.github/workflows/ci.yml:114-117);
  3. 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 production environment 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 follows staging-ok/v<version> (.github/workflows/ci.yml:28, deploy/install.sh:15-21).
  • CHANGELOG.md follows 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).
  • ruff and mypy versions are pinned in the dev extra 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 check is not a gate; the comment explains that the model metadata in migrations/env.py is incomplete (.github/workflows/ci.yml:118-123).
  • Whether the production environment 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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13