固化部署体检与备份合同刷新(08-02-deployment-preflight-backup-contract-refresh)
这个提交包含在:
@@ -7,7 +7,7 @@ operations require explicit approval and evidence.
|
||||
|
||||
## Rules
|
||||
|
||||
- Existing installations use `DEPLOY_SERVER=<ssh-target> bash deploy/trialrun/deploy-existing.sh` only when deployment is approved; the wrapper reads the frozen allowlisted contract and keeps `SYNC_ENV=0`. First initialization or controlled recovery without that unit uses `deploy.sh` with explicit absolute data paths and `DEPLOY_DATA_MODE`; neither path overwrites remote `.env` by default.
|
||||
- Existing installations use `DEPLOY_SERVER=<deploy-ssh-target> DEPLOY_REFRESH_SERVER=<forced-command-ssh-target> bash deploy/trialrun/deploy-existing.sh` only when deployment is approved; the wrapper reads the frozen allowlisted contract, keeps `SYNC_ENV=0`, performs aggregate read-only preflight and refreshes fixed backup build drop-ins after release. First initialization or controlled recovery without that unit uses `deploy.sh` with explicit absolute data paths and `DEPLOY_DATA_MODE`; neither path overwrites remote `.env` by default.
|
||||
- When `server/src/config.js` reads a new environment key, add the same key to
|
||||
`deploy/trialrun/.env.example` before deployment; `deploy/trialrun/deploy.sh`
|
||||
runs `node deploy/trialrun/validate-env-contract.js` at step 0 and must fail
|
||||
@@ -1292,7 +1292,10 @@ bash scripts/qa/run-automatic-backup-contract.sh
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Existing deployment entry: `DEPLOY_SERVER=<ssh-target> bash deploy/trialrun/deploy-existing.sh`.
|
||||
- Existing deployment entry: `DEPLOY_SERVER=<deploy-ssh-target> DEPLOY_REFRESH_SERVER=<forced-command-ssh-target> bash deploy/trialrun/deploy-existing.sh`.
|
||||
- Existing deployment root-refresh identity: `DEPLOY_REFRESH_SERVER=<forced-command-ssh-target>`; it is a separate dedicated key/alias, not sudo and not the ordinary deploy identity.
|
||||
- Aggregate read-only preflight: `deploy/trialrun/check-existing-deployment.sh`.
|
||||
- Root-owned forced-command source/installer: `scripts/refresh-deployment-contract.sh` and `deploy/trialrun/install-deployment-contract-refresh.sh`.
|
||||
- Remote preflight: `deploy/trialrun/preflight-remote.sh` executed over SSH before rsync.
|
||||
- Canonical parameter source: allowlisted `Environment=` values from `hyb-oa-automatic-backup-daily.service`.
|
||||
- Stable image stages: `base`, `client_deps`, `client_build`, `server_deps`, `server_build`.
|
||||
@@ -1302,9 +1305,14 @@ bash scripts/qa/run-automatic-backup-contract.sh
|
||||
- Never derive `REMOTE_DIR` from Compose `com.docker.compose.project.working_dir`; it may name the Compose-file directory rather than the repository root.
|
||||
- The existing-deployment wrapper reads only `REMOTE_DIR`, `REMOTE_ENV_FILE`, `COMPOSE_FILE`, `COMPOSE_PROJECT_NAME`, `POSTGRES_DATA_DIR` and `STORAGE_HOST_DIR`; it does not echo their values.
|
||||
- Before `mkdir` or rsync, existing mode requires repository markers, `PG_VERSION`, storage directory and a regular non-symlink env file when `SYNC_ENV=0`.
|
||||
- Before the fail-fast remote preflight, the existing-deployment wrapper runs all aggregate checks without `set -e`: each check executes in an isolated conditional/subshell, results are emitted in a fixed order, and any failure blocks all writes only after the complete report is produced. Output contains check names and PASS/FAIL only.
|
||||
- The ordinary deploy identity and the root maintenance identity are separate. Root maintenance uses a dedicated key whose only authorized entry is a forced command with forwarding, PTY and user rc disabled. If the same key blob has an unmanaged or unrestricted root entry, installation fails.
|
||||
- The forced-command target is root-owned, non-symlink and self-contained under `/usr/local/sbin`. It accepts exact `probe` and `refresh` commands through `SSH_ORIGINAL_COMMAND`; it never executes the repository, accepts no caller path/unit/SHA, and uses fixed service/timer names.
|
||||
- If the frozen unit points to a missing or non-repository root, recover the canonical source tree from an exact published Git commit into a new dedicated root. Do not choose among stale source directories by timestamp, and do not use `--delete` or carry an environment file into the new root during this bootstrap.
|
||||
- When the frozen `REMOTE_ENV_FILE` is missing, locate candidates from the running Compose configuration and compare each candidate's service/database values with the running containers by per-key digest. Output only key names and PASS/FAIL; never print values or digests. A candidate must pass every container-observable key, including the field-encryption and database identity keys, before root copies it (not moves it) to the canonical root with deployment-user ownership and mode `0600`. If no candidate fully passes, stop and use the environment recovery classification; never use `SYNC_ENV=1` to guess.
|
||||
- After a successful application switch, re-run the automatic-backup and WARCH3 installers with the newly active full `BUILD_SHA` and matching 12-character frontend prefix. Validate all managed service working directories/env paths/build identities and enabled timers, then run one complete backup plus the freshness watchdog; application `/ready` alone does not prove the backup contract followed the release.
|
||||
- When recovering a broken deployment contract or changing frozen paths/project identity, re-run the automatic-backup and WARCH3 installers with the active full `BUILD_SHA` and matching 12-character frontend prefix. Validate all managed service working directories/env paths/build identities and enabled timers, then run one complete backup plus the freshness watchdog; application `/ready` alone does not prove that a controlled recovery is complete.
|
||||
- For normal existing deployments after forced-command installation, the wrapper replaces the manual per-release installer step: after application, smoke and public checks, `refresh` derives the full SHA from the unique active server, atomically writes root-owned build-only drop-ins for the three automatic-backup services and WARCH3, reloads systemd, and verifies four final service SHAs plus four enabled/active timers. Full installers remain authoritative for path or complete-unit changes.
|
||||
- A post-refresh failure does not roll back a healthy application. The wrapper emits `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED`, classifies root transport, missing program, stale unit build or inactive timer, and exits non-zero; only successful refresh may print the complete existing-deployment PASS.
|
||||
- `apk add` and both lockfile `npm ci` steps precede `BUILD_SHA` / `VITE_BUILD_SHA`. Build identity may invalidate application output, never stable dependency installation.
|
||||
- Seed and smoke output reports readiness, counts and booleans; it does not print full login identifiers, contact values, WeCom identifiers, redirect URI or generated credentials.
|
||||
|
||||
@@ -1316,20 +1324,27 @@ bash scripts/qa/run-automatic-backup-contract.sh
|
||||
- Environment copy digest, owner or mode mismatch -> stop before formal deploy and keep the original candidate unchanged.
|
||||
- Existing PGDATA missing `PG_VERSION` or storage missing -> preflight fails before rsync.
|
||||
- Canonical systemd unit missing an allowlisted value -> wrapper fails without calling `deploy.sh`.
|
||||
- An early aggregate item fails -> all independent items still run, the fixed summary reports every FAIL, and no remote write begins.
|
||||
- Root SSH returns transport 255 -> `root_identity_unavailable`; remote 126/127 or missing probe marker -> `refresh_program_unavailable`.
|
||||
- Helper runs but final service build differs -> `unit_build_not_refreshed`; builds match but any timer is disabled/inactive -> `timer_not_active`.
|
||||
- A similarly named environment key such as `OLD_BUILD_SHA` matches the active value while exact `BUILD_SHA` remains stale -> treat the service as stale; build verification parses exact keys and applies the last systemd override.
|
||||
- Post-deploy installer still embeds the previous build SHA, any managed timer is inactive, or the real backup/watchdog fails runtime identity or off-host freshness -> deployment recovery remains incomplete even when `/ready` is green.
|
||||
- Lockfile unchanged but a new SHA reruns dependency installation -> Dockerfile contract test fails.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: wrapper reads the frozen unit contract, preflight passes, Git archive syncs and unchanged dependency layers are cached.
|
||||
- Good: ordinary and forced-command identities pass separately, all aggregate items are reported, deployment passes, root helper updates only fixed build drop-ins, and four timers remain enabled/active.
|
||||
- Good: a broken-root recovery creates a clean published-commit source tree, proves one environment candidate against running containers without exposing values, copies it with mode `0600`, deploys with `SYNC_ENV=0`, then updates the backup contracts and completes a real off-host backup.
|
||||
- Base: first initialization uses reviewed explicit parameters and `DEPLOY_DATA_MODE=initialize`; it does not pretend an existing unit is available.
|
||||
- Bad: inspect a running container's `working_dir`, append another `deploy/trialrun`, then discover the mistake only after rsync.
|
||||
- Bad: choose one of several stale `.env` files by path or modification time, use `SYNC_ENV=1`, or stop after `/ready` while the backup unit still expects the previous build.
|
||||
- Bad: grant NOPASSWD to a repository installer, point forced-command at a deploy-user-writable script, reuse an unrestricted root key, stop aggregation after its first failure, or downgrade refresh failure to a warning.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/deploy-existing.sh deploy/trialrun/preflight-remote.sh`.
|
||||
- `bash scripts/qa/run-deployment-contract-refresh-contract.sh` must prove multi-failure aggregation reaches the final item, output redaction, forced-command exact matching, four failure categories, four build drop-ins and four timer checks.
|
||||
- `node deploy/trialrun/validate-env-contract.js`.
|
||||
- `cd server && npm test -- tests/unit/scripts/deployment-safety-contract.test.js`.
|
||||
- Synthetic fixtures must prove wrong root, missing/symlink env and invalid data fail; static ordering must prove preflight precedes rsync and dependency stages precede build args.
|
||||
@@ -1349,12 +1364,13 @@ REMOTE_DIR="$(docker inspect <server> --format '<working_dir-label>')" \
|
||||
|
||||
```bash
|
||||
DEPLOY_SERVER=<approved-ssh-target> \
|
||||
DEPLOY_REFRESH_SERVER=<approved-forced-command-target> \
|
||||
bash deploy/trialrun/deploy-existing.sh
|
||||
|
||||
# If contract recovery was required, update the backup units with the active
|
||||
# full SHA after the switch, then prove both creation and freshness.
|
||||
systemctl start hyb-oa-automatic-backup-daily.service
|
||||
systemctl start hyb-oa-automatic-backup-watchdog.service
|
||||
# A normal release ends here: the wrapper refreshes the fixed build drop-ins
|
||||
# and verifies all four timers. Do not manually start backup services on every
|
||||
# release. If contract recovery was required, follow the separately approved
|
||||
# recovery runbook to prove one complete backup and off-host freshness.
|
||||
```
|
||||
|
||||
## Scenario: Environment Recovery Classification And Secret Custody
|
||||
|
||||
在新工单中引用
屏蔽一个用户