docs(staging): add personal report acceptance and rollback runbook
Staging Backend Quality Gate / validate (pull_request) Failing after 15m33s
Staging Backend Quality Gate / publish (pull_request) Has been skipped

This commit is contained in:
Jesse
2026-08-06 12:55:26 +08:00
parent d89073d296
commit c949b224fe
5 changed files with 203 additions and 0 deletions
+5
View File
@@ -19,3 +19,8 @@ ADMIN_EMAILS=<comma-separated-staging-admin-emails>
EPAY_CONFIG_ENCRYPTION_KEY=<independent-openssl-rand-base64-32-output>
EPAY_CHAT_ENABLED=false
JYOTISH_DYNAMIC_RECTIFICATION_TOKEN=<independent-openssl-rand-base64-32-output>
# Personal reports are staging-only in this rollout. The web service receives
# these server-side values through env_file; they are not NEXT_PUBLIC values.
PERSONAL_REPORT_ENABLED=true
PERSONAL_REPORT_DAILY_LIMIT=5
+18
View File
@@ -106,4 +106,22 @@ fi
require_selector EPAY_CHAT_ENABLED false
require_literal JYOTISH_DYNAMIC_RECTIFICATION_TOKEN 32
personal_report_enabled_count="$(grep -Ec '^PERSONAL_REPORT_ENABLED=' "$ENV_FILE" || true)"
personal_report_enabled="$(grep -E '^PERSONAL_REPORT_ENABLED=' "$ENV_FILE" || true)"
personal_report_enabled="${personal_report_enabled#*=}"
if [ "$personal_report_enabled_count" -ne 1 ] ||
[[ "$personal_report_enabled" != "true" && "$personal_report_enabled" != "false" ]]; then
echo "invalid staging personal report setting: PERSONAL_REPORT_ENABLED" >&2
exit 1
fi
personal_report_daily_limit_count="$(grep -Ec '^PERSONAL_REPORT_DAILY_LIMIT=' "$ENV_FILE" || true)"
personal_report_daily_limit="$(grep -E '^PERSONAL_REPORT_DAILY_LIMIT=' "$ENV_FILE" || true)"
personal_report_daily_limit="${personal_report_daily_limit#*=}"
if [ "$personal_report_daily_limit_count" -ne 1 ] ||
[[ ! "$personal_report_daily_limit" =~ ^[1-9][0-9]*$ ]]; then
echo "invalid staging personal report setting: PERSONAL_REPORT_DAILY_LIMIT" >&2
exit 1
fi
echo "staging environment selectors: valid"
+168
View File
@@ -0,0 +1,168 @@
# Personal Report Staging Acceptance and Rollback
This runbook covers the staging-only rollout of `ReportDocument v1`, personal report generation, the report reader, D1 SVG rendering, and browser print-to-PDF. It does **not** authorize a production migration or deployment.
## Release identity
- Pre-change application baseline: `49da8f916960030d5760d8dedf4e77820732a527`
- Read-only upstream Skill source commit: `unknown` (the local source has no usable Git metadata)
- Read-only upstream source tree SHA-256: `9034e1967032d09c7fbae83fc2205f7e75e8ad482c5f9eba1bf309fe30aef5bb`
- Packaged `SKILL.md` SHA-256: read from `references/upstream/yinduzhanxing/source-manifest.json`
- Deployment SHA: use the exact full SHA shared by `main` and `staging`; never substitute a short SHA or mutable image tag.
- Application rollback target: `49da8f916960030d5760d8dedf4e77820732a527`, subject to successful gate artifact retention.
## Staging configuration
The server-owned `/opt/jyotisha-staging/.env.staging` must remain mode `0600`. Do not print or copy the file. In addition to the existing self-hosted identity settings, configure:
```dotenv
PERSONAL_REPORT_ENABLED=true
PERSONAL_REPORT_DAILY_LIMIT=5
```
`PERSONAL_REPORT_ALLOWED_ORIGINS` is normally omitted because browser requests are same-origin. Add it only for an explicitly reviewed origin. `JYOTISH_SKILL_SNAPSHOT_SHA256` and `JYOTISH_SKILL_SOURCE_COMMIT` are optional deployment pins; without a valid override, the web bundle uses the statically packaged, validated source manifest and fails closed if its SHA is invalid.
Validate without logging values:
```bash
cd /opt/jyotisha-staging
./deploy/validate-staging-env.sh .env.staging
docker compose --env-file .env.staging -f deploy/docker-compose.server.yml config --quiet
```
The report API reads these values server-side through Compose `env_file`. They must not be changed to `NEXT_PUBLIC_*` build arguments.
## Exact-SHA release sequence
Follow `deploy/README.md`; the short form is:
1. Confirm the reviewed `main` full SHA and fast-forward `staging` to the same SHA.
2. Wait for `Staging Backend Quality Gate` on the `staging` push to succeed and publish the immutable API/web digest manifest.
3. Let the automatic read-only deployment check stop on pending migrations.
4. Run `Migrate Staging Database` manually from the `main` controller with the exact full SHA.
5. Confirm the migration ledger contains `20260806000000_personal_reports.sql`.
6. Run `Deploy staging` manually with that same SHA and `allow_rollback=false`.
7. Verify `https://staging.jyotisha.chat/api/health` reports that exact SHA and both private services healthy.
Do not deploy the application before the additive migration. Do not trigger production workflows.
## Automated acceptance
Before pushing, record the literal command and outcome for:
```bash
python3 -m pytest -q \
tests/test_cross_project_contract.py \
tests/test_cross_project_sync_status.py \
tests/test_import_yinduzhanxing.py
python3 -m pytest -q \
tests/test_unified_consultation_orchestrator.py \
tests/test_report_orchestrator_reader_contract.py \
tests/test_consultation_consumer_context.py \
tests/test_personal_report_contract.py \
tests/test_supabase_migration_versions.py
npm test --prefix frontend
npm run lint --prefix frontend
npm run build --prefix frontend
```
If the complete frontend suite has environment-only failures, preserve their exact count and cause; do not report the suite as green. The personal-report-focused test set, TypeScript check, Python contract test, migration contract, production build, and `git diff --check` must pass before release.
## Synthetic browser acceptance
Use only synthetic accounts and synthetic birth facts. Never paste real birth data into tickets, logs, screenshots, or this document.
Prepare these profiles:
1. `confirmed` with an exact synthetic active time.
2. `accepted` with a user-adopted synthetic candidate time.
3. A report whose evidence contains blocked/conflicting techniques.
4. A second synthetic owner account for isolation checks.
For each supported owner profile:
1. Sign in and complete a personal consultation with workflow evidence.
2. Confirm the “生成个人报告” CTA is visible only for `accepted` or `confirmed` active time. It must be hidden for `reported` and unaccepted `candidate` states.
3. Generate one report. Confirm the button prevents duplicate in-flight clicks and the app navigates to `/reports/<uuid>`.
4. Confirm content order is summary, thematic narrative, then evidence appendix.
5. Confirm D1 renders from real document facts. D9/D10 must be absent when their complete facts are unavailable.
6. Confirm blocked claims use uncertainty language and contain no deterministic prediction.
7. Reload the report URL and confirm the same validated document is returned.
8. Attempt the report URL as the second synthetic owner; expect `404`/not found and no document data.
9. Delete as the owner and confirm subsequent retrieval is not found.
10. Generate with the same request ID and payload to confirm replay behavior; a different payload under the same ID must return `409 report_request_conflict`.
PDF/browser matrix:
| Client | Required check | Status before user handoff |
| --- | --- | --- |
| Desktop Chrome | Print → Save as PDF; SVG sharp; Chinese text intact; tables not clipped | pending manual |
| macOS Safari | Print → Save as PDF; pagination and fonts | pending manual |
| iPhone Safari | Share/Print flow and mobile hint | pending manual |
| WeChat in-app browser | Export guidance is understandable | pending manual |
For a long synthetic document, target 2040 printed pages and verify that print CSS expands the evidence appendix as designed. The report action must only wait for `document.fonts.ready` and call `window.print()`.
## Security and server assertions
Verify logged-out and cross-owner behavior without recording tokens:
```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://staging.jyotisha.chat/api/account
curl -sS -o /dev/null -w '%{http_code}\n' https://staging.jyotisha.chat/api/reports/00000000-0000-4000-8000-000000000000
```
Expected logged-out status is `401`. Authenticated owner/non-owner checks must be performed in the browser or with ephemeral local credentials that are never pasted into logs.
On the host, confirm no product PDF browser runtime exists:
```bash
cd /opt/jyotisha-staging
docker compose --env-file .env.staging -f deploy/docker-compose.server.yml exec -T web \
sh -lc '! command -v chromium && ! command -v chromium-browser && ! command -v google-chrome'
docker compose --env-file .env.staging -f deploy/docker-compose.server.yml exec -T web \
sh -lc '! ps aux | grep -E "[c]hromium|[p]laywright|[p]uppeteer"'
```
Inspect logs only for stable codes and operational state. Do not log or copy report text, prompts, birth facts, JWTs, cookies, model keys, exception stacks, or the staging env file.
## Resource evidence
Record before, during one generation, and during two synthetic users attempting concurrent generation:
```bash
cd /opt/jyotisha-staging
docker stats --no-stream
docker compose --env-file .env.staging -f deploy/docker-compose.server.yml ps
docker compose --env-file .env.staging -f deploy/docker-compose.server.yml logs --tail=200 api web caddy
```
Capture only aggregate CPU, memory, container restart count, request duration, stable failure state, and serialized report byte size. Confirm:
- one generation per user at a time;
- a second concurrent request for the same user returns the stable in-progress response;
- serialized `ReportDocument` is at most 1.5 MiB;
- no OOM or container restart;
- no Chromium/Playwright server process.
One successful sample does not establish high-concurrency capacity.
## Rollback
### Application
Run `Deploy staging` from the trusted `main` controller using the previous known-good full SHA and `allow_rollback=true`. The selected SHA must still have a successful quality-gate artifact. Do not use a mutable tag or manually overwrite the server tree.
### Database
The personal report migration is additive. Application rollback intentionally leaves `public.personal_reports`, its grants, constraints, indexes, and RLS policies in place. Do not run a destructive down migration. Delete only explicitly identified synthetic test rows if cleanup is required.
### Feature pause
If generation must stop while the current application remains deployed, set `PERSONAL_REPORT_ENABLED=false` in the server-owned staging env under the deployment lock and recreate only the web service after validation. This is an operational containment action, not a substitute for an application rollback. Restore `true` only after the incident is resolved.
### Skill
Revert the commercial repository import/semantic-merge commits as reviewed. Do not modify the read-only upstream source directory. Preserve the import manifest and record that the target revision was rolled back; never rewrite provenance history.
+10
View File
@@ -249,6 +249,8 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi
"EPAY_CONFIG_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"EPAY_CHAT_ENABLED=false",
"JYOTISH_DYNAMIC_RECTIFICATION_TOKEN=dynamic-token-that-is-at-least-32-bytes",
"PERSONAL_REPORT_ENABLED=true",
"PERSONAL_REPORT_DAILY_LIMIT=5",
];
const run = () =>
spawnSync("bash", [validator, envFile], { encoding: "utf8" });
@@ -276,6 +278,14 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi
assert.notEqual(run().status, 0);
writeEnv(validSelectors.map((line) => line.startsWith("EPAY_CHAT_ENABLED=") ? "EPAY_CHAT_ENABLED=true" : line));
assert.notEqual(run().status, 0);
writeEnv(validSelectors.map((line) => line.startsWith("PERSONAL_REPORT_ENABLED=") ? "PERSONAL_REPORT_ENABLED=false" : line));
assert.equal(run().status, 0);
writeEnv(validSelectors.map((line) => line.startsWith("PERSONAL_REPORT_ENABLED=") ? "PERSONAL_REPORT_ENABLED=TRUE" : line));
assert.notEqual(run().status, 0);
writeEnv(validSelectors.map((line) => line.startsWith("PERSONAL_REPORT_DAILY_LIMIT=") ? "PERSONAL_REPORT_DAILY_LIMIT=0" : line));
assert.notEqual(run().status, 0);
writeEnv(validSelectors.map((line) => line.startsWith("PERSONAL_REPORT_DAILY_LIMIT=") ? "PERSONAL_REPORT_DAILY_LIMIT=5x" : line));
assert.notEqual(run().status, 0);
writeEnv(validSelectors);
const shellOverride = spawnSync(
@@ -799,6 +799,8 @@ test("public rectification rollout enables the semantic agent and recreates web
"EPAY_CONFIG_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"EPAY_CHAT_ENABLED=false",
`JYOTISH_DYNAMIC_RECTIFICATION_TOKEN=${"t".repeat(32)}`,
"PERSONAL_REPORT_ENABLED=true",
"PERSONAL_REPORT_DAILY_LIMIT=5",
"KEEP_ME=unchanged",
"RECTIFICATION_V3_CREATE_ENABLED=false",
"RECTIFICATION_V3_MIGRATIONS_READY=false",