Cluster ERP has four environments: the development workstation, continuous integration, a staging node, and a production node. A release reaches production only after the same tag ran on staging. This page explains the chain and the code that enforces it in deploy/install.sh, .github/workflows/ci.yml, and src/cluster_erp/server/staging_fixtures.py. Step-by-step installation is in install a Linux node; the CI jobs are in CI pipeline and release.
In short¶
- A node is installed with
deploy/install.sh --tag <tag> --domain <domain> --role staging|production; the role is mandatory (deploy/install.sh:77-83). - A production install refuses a tag that has no annotated promotion tag
staging-ok/<tag>on the same commit (deploy/install.sh:101-107). - A node records its role at the first install and refuses any other role later (
deploy/install.sh:122-128). - Continuous integration runs lint, lock, and test jobs on every push, and a deploy job on version tags that waits for approval in the
productionenvironment (.github/workflows/ci.yml:25-28,.github/workflows/ci.yml:126-136). - The browser battery is not part of continuous integration; it runs against the staging node before the promotion tag is set (
.github/workflows/ci.yml:21-22).
How it works¶
The chain has five gates. The diagram shows them in order.
flowchart LR dev["Workstation"] --> ci["CI jobs lint, lock, test"] ci --> tag["Version tag v*"] tag --> stg["install.sh --role staging"] stg --> battery["_run_battery.mjs on staging"] battery --> ok["Tag staging-ok/v*"] ok --> prod["install.sh --role production"] tag --> deploy["CI deploy job, approval"] deploy --> prod
The same content in prose:
- A developer works on a workstation. On Windows the server runs as one process whatever
server_workerssays (src/cluster_erp/server/main.py:27-35). - Every push and pull request runs the
lint,lock, andtestjobs (.github/workflows/ci.yml:37-124). - A version tag
v*is pushed. The workflow runs the same gates and then thedeployjob (.github/workflows/ci.yml:126-129). - The operator installs the tag on the staging node with
--role staging. A staging install needs no promotion record (deploy/install.sh:101,tests/test_install_role_gate.py:145). - The browser battery runs against the staging node. The runner
web-client/_run_battery.mjsruns every_e2e_*.mjssuite in sequence and exits non-zero when one suite fails or prints no result (web-client/_run_battery.mjs:1-20). Suites that need a second PostgreSQL server or local processes are left out with--exclude(web-client/_run_battery.mjs:35-38). - When the battery passes, the operator records the promotion as a second annotated tag
staging-ok/<tag>on the same commit and pushes it (deploy/install.sh:15-21). - The production install (by hand or by the CI
deployjob) resolves both tags on the remote. It stops when the record is missing or points at another commit (deploy/install.sh:85-107).
Node role¶
At the first install the script writes the role into the file named by ROLE_FILE (deploy/install.sh:58, deploy/install.sh:122-128). A later install with a different role stops with the message that a node never changes role. The staging test accounts are created only on a node whose recorded role is staging (src/cluster_erp/server/staging_fixtures.py:92-105). See demo and staging data.
Approval of the deploy job¶
The deploy job is bound to the GitHub environment production and to a concurrency group that never cancels a running deploy (.github/workflows/ci.yml:131-136). The workflow header states that the environment is configured with required reviewers, so a person approves each deploy in the Actions screen (.github/workflows/ci.yml:7-12). The reviewer setting itself lives in the repository settings: Not determined from code. The job runs install.sh --role production on the node, so the promotion gate of step 7 applies to it too (.github/workflows/ci.yml:155-156).
Why it is built this way¶
- "production runs a tagged release, never a branch tip" (
deploy/install.sh:77). - "a release reaches production only through staging" (
deploy/install.sh:81). - "a node keeps the role of its first install; staging data never meets production" (
deploy/install.sh:122). - "a tag alone never reaches a customer node" (
.github/workflows/ci.yml:9-10).
Rules and boundaries¶
- The promotion gate is tested by
tests/test_install_role_gate.py(seven tests, counted asdef test_lines). - An install is reported live only after
/readyanswers HTTP 200 and the residency check passes (deploy/install.sh:247-259). See security.txt and readiness and data residency check. - The production install takes a backup before it migrates (
deploy/install.sh:214-226).
Interaction with other parts¶
- Tenant lifecycle: on a multi-tenant node the install runs the fleet upgrade runner.
- Database migrations: the schema change of a release.
- Backups and restore: the pre-upgrade backup.
Limits and open points¶
- Continuous integration waives the live guards with
CLUSTER_SKIP_LIVE_TESTS=1(.github/workflows/ci.yml:18-20,.github/workflows/ci.yml:99). - Which hosts form the staging and production nodes is deployment data and is omitted under the security disclosure policy.
- The list of past promotions is kept outside the wiki and is not reproduced here.
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| promotion record | staging-ok/<tag> |
An annotated tag on the release commit that records a passed staging battery. | terfi kaydı | deploy/install.sh:15-21 |
| node role | ROLE_FILE |
The role, staging or production, written at a node's first install and never changed. | düğüm rolü | deploy/install.sh:122-128 |
| battery | _run_battery.mjs |
The sequential run of every browser e2e suite against one server. | batarya | web-client/_run_battery.mjs:1-20 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.