change in to merge
CI/CD - ERP Core / detect (pull_request) Failing after 13m11s
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m17s
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled
CI/CD - ERP Core / detect (pull_request) Failing after 13m11s
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m17s
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled
This commit is contained in:
+21
-19
@@ -2,7 +2,7 @@
|
||||
|
||||
> **What this is:** how code gets from a pull request to `erp.hexdive.com` (the `test-erp.hexdive.com` environment is on hold), how the server is set up, and how to operate it (config changes, rollback, backups).
|
||||
> **Navigation:** you arrived from `00-CORE.md` or `01-DOC-GUIDE.md`. Security posture: `02-SECURITY.md` (AR-06 covers the secrets history).
|
||||
> **Files (active):** `.gitea/workflows/ci-cd.yml` (PR checks + production deploy, one file) · `deploy/scripts/{common,ci-detect,ci-backend,migration-scan,wait-healthy}.sh` · `deploy/systemd/erpcore.service` · `deploy/apache/erp.hexdive.com.conf` · `deploy/env/erpcore.env.example`.
|
||||
> **Files (active):** `.gitea/workflows/ci-cd.yml` (PR checks + production deploy, one file) · `deploy/scripts/{common,ci-detect,ci-backend,migration-scan,wait-healthy}.sh` · `deploy/systemd/erpcore.service` · `deploy/apache/erp.hexdive.com.conf`. Runtime config lives in the workflow's `env:` block — there is no env file on the server.
|
||||
> **Files (on hold):** `deploy/on-hold/` — the test + release pipeline, see its README.
|
||||
|
||||
---
|
||||
@@ -36,8 +36,9 @@ The deploy job checks `pull_request.base.ref == 'production'` itself, because th
|
||||
| 2 | Detect changes | always | compare folder **tree hashes** base → merge for `Backend/ERPCore`, `Frontend/erp-system`, `Backend/ERPCore/Migrations`. Works for merge, squash and rebase merges. Manual runs use the `deploy_backend` / `deploy_frontend` checkboxes. |
|
||||
| 3 | Restore, Publish | backend changed | `dotnet restore` + `dotnet publish -r linux-x64 --self-contained` into `/tmp/erp-deploy` (on the host, like Fitness) |
|
||||
| 4 | Backup database | backend **and** migrations changed | `pg_dump -Fc ERPCore` → `/var/backups/erpcore/`, dumps older than 30 days deleted |
|
||||
| 5 | Guard data paths | backend changed | fail if `FileStorage__RootPath` / `LogFile__Path` in `/etc/erpcore/erpcore.env` are missing, relative, or inside the publish folder |
|
||||
| 6 | Stop → Copy → Fix ownership → Start | backend changed | `systemctl stop erpcore.service` · empty `/home/erp/core/publish` and copy the build · `chown erp:erp`, 755/644, `+x ERPCore` · `systemctl start` |
|
||||
| 5 | Install unit + write config | backend changed | install `deploy/systemd/erpcore.service`, write the `ASPNETCORE_URLS` drop-in, `daemon-reload` + `enable`. Validates first, while the **old** release is still serving: fails on a `CHANGE_ME` URL, a missing secret, a `;` in the DB password, or a storage/log path inside the publish folder |
|
||||
| 6 | Stop → Copy → Fix ownership → Start | backend changed | `systemctl stop erpcore.service` · empty `/home/erp/core/publish` and copy the build · `chown erpuser:erpuser`, 755/644, `+x ERPCore` · `systemctl start` |
|
||||
| 6b | Write `appsettings.Production.json` | backend changed | generate it into the publish folder from the `env:` block + Gitea secrets (mode 640 `root:erpuser`), after the chmod pass so it is not world-readable. It **merges over** the committed `appsettings.json`, which keeps `Auth:RsaPublicKeyXml`, `Issuer`, `Audience` and `FileStorage:MaxSizeBytes`. Not in git — `.gitignore` ignores `appsettings.*.json` |
|
||||
| 7 | Verify backend | backend changed | `deploy/scripts/wait-healthy.sh`: waits for `/health` 200. A crash (unit failed / auto-restarted) fails within seconds; otherwise it waits up to 300 s, because migrations run at startup. Failure prints the journal and turns the job **red**. |
|
||||
| 8 | Build frontend image → Replace container | frontend changed | `docker build -t erp-web:latest Frontend/erp-system` (build inside the image, like Client_New_Fe) · recreate `erp-web` on `127.0.0.1:3020`, 512 MB, `--restart unless-stopped` |
|
||||
| 9 | Verify frontend → Remove old images | frontend changed | `GET /login` within 30 s or print container logs and fail · `docker image prune -f`, `docker builder prune` older than 7 days |
|
||||
@@ -68,33 +69,31 @@ dotnet --list-sdks # needs a 10.0.x SDK; install it next
|
||||
# (Fitness/AuthHex have no global.json, so they will build with SDK 10 too once it is the newest — normally fine)
|
||||
|
||||
# Service user + folders. Uploads, logs and backups live OUTSIDE the publish folder, which every deploy empties.
|
||||
useradd --create-home --home-dir /home/erp --shell /sbin/nologin erp
|
||||
install -d -m 755 -o erp -g erp /home/erp/core /home/erp/core/publish
|
||||
install -d -m 750 -o erp -g erp /var/lib/erpcore/hr-documents /var/log/erpcore
|
||||
# (The deploy creates the data dirs too, but make them now so a first run has nothing to fix.)
|
||||
useradd --create-home --home-dir /home/erp --shell /sbin/nologin erpuser
|
||||
install -d -m 755 -o erpuser -g erpuser /home/erp/core /home/erp/core/publish
|
||||
install -d -m 750 -o erpuser -g erpuser /var/lib/erpcore/hr-documents /var/log/erpcore
|
||||
install -d -m 700 /var/backups/erpcore
|
||||
install -d -m 750 -o root -g erp /etc/erpcore
|
||||
|
||||
# Database (Postgres 16, localhost only)
|
||||
# Database (Postgres 16, localhost only). This password goes into the Gitea secret ERP_DB_PASSWORD.
|
||||
sudo -u postgres psql -c "CREATE ROLE erpcore LOGIN PASSWORD 'CHANGE_ME';" -c 'CREATE DATABASE "ERPCore" OWNER erpcore;'
|
||||
|
||||
# Settings + secrets (fill in CHANGE_ME / ports — keys in §5)
|
||||
install -m 640 -o root -g erp deploy/env/erpcore.env.example /etc/erpcore/erpcore.env
|
||||
|
||||
# Service
|
||||
cp deploy/systemd/erpcore.service /etc/systemd/system/ && systemctl daemon-reload && systemctl enable erpcore.service
|
||||
# Service: only `enable` by hand. The deploy installs the unit and writes the config drop-in itself,
|
||||
# so there is NO file to create under /etc/erpcore — that directory is not used any more.
|
||||
# nothing to do here — the deploy installs the unit, writes the config and runs `systemctl enable`
|
||||
|
||||
# Apache + TLS (needs SELinux httpd_can_network_connect=on)
|
||||
cp deploy/apache/erp.hexdive.com.conf /etc/httpd/conf.d/ && apachectl configtest && systemctl reload httpd
|
||||
certbot --apache -d erp.hexdive.com
|
||||
```
|
||||
**Gitea:** Actions enabled for the repo · the `linux` runner available to `New_REP_SYSTEM` · secret **`ERPSECRET`** (a token that can read the repo) · branch protection on `production`: require PR, 1 approval, required check `ci-ok` (it appears as `CI/CD - ERP Core / ci-ok …` after the first PR run; a pattern like `CI/CD - ERP Core / ci-ok*` matches it), no direct push. `Dev`: require PR + the same check.
|
||||
**Gitea:** Actions enabled for the repo · the `linux` runner available to `New_REP_SYSTEM`, with its label mapped to **`host`** (§A.4) · secrets **`ERPSECRET`** (repo-read token), **`ERP_DB_PASSWORD`** (the `erpcore` role's password) and **`ERP_GL_API_KEY`** · branch protection on `production`: require PR, 1 approval, required check `ci-ok` (it appears as `CI/CD - ERP Core / ci-ok …` after the first PR run; a pattern like `CI/CD - ERP Core / ci-ok*` matches it), no direct push. `Dev`: require PR + the same check.
|
||||
|
||||
**First deploy:** merge a PR that touches both folders, or use the manual run with both boxes ticked. If `erpcore.service` fails with `status=203/EXEC`, run `restorecon -Rv /home/erp/core/publish` (SELinux label).
|
||||
|
||||
## A.3 Operations
|
||||
| Task | How |
|
||||
|---|---|
|
||||
| Config change | edit `/etc/erpcore/erpcore.env` → Run workflow with **deploy_backend** (or `systemctl restart erpcore` + `bash deploy/scripts/wait-healthy.sh`) |
|
||||
| Config change | edit the `env:` block of `.gitea/workflows/ci-cd.yml` (or the Gitea secret, for the two credentials) → merge → Run workflow with **deploy_backend**. `appsettings.Production.json` is regenerated on every backend deploy. |
|
||||
| Redeploy after a failure | fix the cause → Run workflow with the failed side ticked |
|
||||
| Roll back | merge a revert PR into `production` (it deploys like any change); see the limits above |
|
||||
| Logs | `journalctl -u erpcore -f` · `/var/log/erpcore/erpcore-*.log` · `docker logs -f erp-web` · `/var/log/httpd/erp.hexdive.com-error.log` |
|
||||
@@ -107,7 +106,7 @@ certbot --apache -d erp.hexdive.com
|
||||
| Job stays "Waiting" | runner not available to this repo/org, or offline |
|
||||
| **Every job red, `Set up job` took minutes, every later step 0 s** | **the `linux` label resolves to a container, not the host** — see below |
|
||||
| "Guard data paths" fails | `FileStorage__RootPath` / `LogFile__Path` missing, relative or inside `/home/erp/core/publish` |
|
||||
| Verify backend fails in seconds with `Missing required configuration …` | a blank key in `/etc/erpcore/erpcore.env` |
|
||||
| Verify backend fails in seconds with `Missing required configuration …` | a key is empty in the generated config — `jq 'paths(scalars) | join(":")' /home/erp/core/publish/appsettings.Production.json` lists the keys it actually got (keys only, no values) |
|
||||
| Verify backend times out after 300 s | DB unreachable (`/health` 503) or a very long migration. **Not** AuthHex/GL: `/health` is `AddDbContextCheck` only, so it depends on Postgres alone (§A.5) |
|
||||
| Login loops back to `/login` | not on HTTPS (cookies are Secure) |
|
||||
| Nothing deployed after a merge | the PR changed neither `Backend/ERPCore` nor `Frontend/erp-system` (the detect step prints the flags) |
|
||||
@@ -122,7 +121,7 @@ certbot --apache -d erp.hexdive.com
|
||||
| `no space left on device` | disk full | `docker system prune`, clear old dumps in `/var/backups/erpcore` |
|
||||
| `failed to create container` / dial or TLS errors | docker daemon or registry unreachable | fix the daemon / egress |
|
||||
|
||||
A docker-mapped runner is wrong here regardless of whether the pull succeeds: the deploy job calls `systemctl`, `runuser -u postgres pg_dump`, `chown erp:erp` and `docker build` **on the host**. Same requirement as the Fitness workflows, which is why their `linux` label is `host`.
|
||||
A docker-mapped runner is wrong here regardless of whether the pull succeeds: the deploy job calls `systemctl`, `runuser -u postgres pg_dump`, `chown erpuser:erpuser` and `docker build` **on the host**. Same requirement as the Fitness workflows, which is why their `linux` label is `host`.
|
||||
|
||||
```bash
|
||||
systemctl cat act_runner | grep -i config # find the --config path
|
||||
@@ -141,7 +140,7 @@ Worth knowing before blaming a red deploy on an unwired dependency:
|
||||
- `MapHealthChecks("/health")` has **no fallback authorization policy** — it is anonymous, so `wait-healthy.sh` never gets a 401.
|
||||
- The only registered check is `AddDbContextCheck<ErpDbContext>()`, so `/health` proves **Postgres** is reachable and nothing else.
|
||||
- The AuthHex and GL typed clients are configured in their `AddHttpClient` delegates, which run **per request**, not at startup. An unreachable AuthHex or GL breaks `/api/v1/auth/*` at runtime but does **not** stop the service or turn `/health` red.
|
||||
- Startup hard-fails on five blank keys (`Program.cs`, `!IsDevelopment()`). Four come from `/etc/erpcore/erpcore.env`; the fifth, `Auth:RsaPublicKeyXml`, ships with a real value in `appsettings.json`. Blank `Auth__RequiredUserTypeCode` / `RequiredRoleCode` is deliberate — the door policy then requires only a valid ERP token.
|
||||
- Startup hard-fails on five blank keys (`Program.cs`, `!IsDevelopment()`). Four come from the generated `appsettings.Production.json`; the fifth, `Auth:RsaPublicKeyXml`, ships with a real value in `appsettings.json`. Blank `Auth__RequiredUserTypeCode` / `RequiredRoleCode` is deliberate — the door policy then requires only a valid ERP token.
|
||||
|
||||
So a backend deploy is expected to go **green with AuthHex still unwired**. `/health` 200 while `/api/v1/auth/login` returns 502/504 is the correct intermediate state, not a failed deploy.
|
||||
|
||||
@@ -205,7 +204,7 @@ Git stores every folder as a **tree object** whose id is a hash of its exact con
|
||||
|
||||
## 4. Server bootstrap (one time)
|
||||
|
||||
> **On hold — do not follow this to set up production.** This bootstraps the two-environment layout: service user **`erpcore`** under `/opt/erpcore`. The active production deploy needs user **`erp`** under `/home/erp/core/publish` (`chown erp:erp` in the workflow, `User=erp` in `erpcore.service`) — use **§A.2** instead. §4.0 (Gitea prerequisites), §4.1 (runner, host tools, swap), §4.3 (Postgres) and §4.7 (branch protection) apply to both; §4.2 does not.
|
||||
> **On hold — do not follow this to set up production.** This bootstraps the two-environment layout: service user **`erpcore`** under `/opt/erpcore`. The active production deploy needs user **`erpuser`** under `/home/erp/core/publish` (`chown erpuser:erpuser` in the workflow, `User=erpuser` in `erpcore.service`) — use **§A.2** instead. §4.0 (Gitea prerequisites), §4.1 (runner, host tools, swap), §4.3 (Postgres) and §4.7 (branch protection) apply to both; §4.2 does not.
|
||||
|
||||
Run as root on the VPS. Keep names/ports in sync with §2.
|
||||
|
||||
@@ -256,6 +255,9 @@ SQL
|
||||
Each role **owns** its database (migrations run DDL at startup) and cannot touch the other. Postgres stays localhost-only; `pg_hba.conf` already allows `127.0.0.1/32` with scram-sha-256.
|
||||
|
||||
### 4.4 Environment files → §5
|
||||
> `deploy/env/erpcore.env.example` was **deleted** when active production moved its config into the
|
||||
> `ci-cd.yml` `env:` block + a systemd drop-in (§A.1 step 5). Restoring this two-environment design means
|
||||
> recreating that template (it is in git history) or adopting the drop-in approach per environment.
|
||||
```bash
|
||||
install -m 640 -o root -g erpcore deploy/env/erpcore.env.example /etc/erpcore/test.env
|
||||
install -m 640 -o root -g erpcore deploy/env/erpcore.env.example /etc/erpcore/release.env
|
||||
|
||||
Reference in New Issue
Block a user