feat: package calendar bootstrap tool

This commit is contained in:
2026-08-23 01:55:54 +08:00
parent 6634b6c4af
commit 36ac6ee34e
4 changed files with 25 additions and 15 deletions

View File

@@ -13,6 +13,12 @@ REDIS_PASSWORD=change-me-with-a-different-random-hex-value
# Absolute host path to the immutable first-import archive. # Absolute host path to the immutable first-import archive.
IMPORT_FILE=/srv/zarinpal-data/challenge_data.csv.gz IMPORT_FILE=/srv/zarinpal-data/challenge_data.csv.gz
# Reviewed calendar bootstrap input and non-secret audit identities.
CALENDAR_FILE=/srv/zarinpal-data/reviewed-calendar.json
CALENDAR_CURRENT_JALALI_YEAR=1405
CALENDAR_REVIEWER=change-me-independent-reviewer
CALENDAR_OPERATOR=change-me-platform-operator
# Optional backup settings. These are reserved for the backup/restore scripts. # Optional backup settings. These are reserved for the backup/restore scripts.
S3_BACKUP_BUCKET= S3_BACKUP_BUCKET=
S3_BACKUP_PREFIX=backup/zarinpal-analytics S3_BACKUP_PREFIX=backup/zarinpal-analytics

View File

@@ -38,11 +38,11 @@ cp frontend/.env.production.example frontend/.env
chmod 600 .env backend/.env frontend/.env chmod 600 .env backend/.env frontend/.env
``` ```
Replace every `change-me` value. Generate URL-safe secrets with `openssl rand -hex 32`. The root file owns the PostgreSQL and Redis passwords; Compose injects their connection settings into the Go containers. The frontend file contains public build-time switches only. Replace every `change-me` value. Generate URL-safe secrets with `openssl rand -hex 32`. The root file owns the PostgreSQL and Redis passwords plus the reviewed-calendar path and non-secret audit identities; Compose injects their connection settings into the Go containers. The frontend file contains public build-time switches only.
## First production start ## First production start
Before adding/reloading the Caddy route, validate and build the stack, import the immutable dataset, create the demo account, and only then start the public upstreams: Before adding/reloading the Caddy route, place the independently reviewed calendar JSON at `CALENDAR_FILE`, make it read-only, and confirm that its `sourceChecksum` is not a review placeholder. Then validate and build the stack, import the immutable dataset, bootstrap the reviewed calendar, create the demo account, and only then start the public upstreams:
```bash ```bash
docker compose config --quiet docker compose config --quiet
@@ -50,6 +50,7 @@ docker compose build backend frontend
docker compose up -d postgres redis docker compose up -d postgres redis
docker compose --profile tools run --rm migrate docker compose --profile tools run --rm migrate
docker compose --profile import run --rm importer docker compose --profile import run --rm importer
docker compose --profile tools run --rm calendar
docker compose --profile tools run --rm admin docker compose --profile tools run --rm admin
docker compose up -d backend worker frontend docker compose up -d backend worker frontend
docker compose ps docker compose ps
@@ -68,18 +69,19 @@ git -C frontend pull --ff-only origin main
bash scripts/deploy.sh bash scripts/deploy.sh
``` ```
Normal deploys run migrations but never run the importer or admin command. Normal deploys run migrations but never run the importer, calendar bootstrap, or admin command.
## Re-running one-off tools ## Re-running one-off tools
`IMPORT_FILE` in the root `.env` is the absolute host path of the immutable `challenge_data.csv.gz`. If recovery requires the idempotent importer or the demo account must be reset, run them explicitly: `IMPORT_FILE` in the root `.env` is the absolute host path of the immutable `challenge_data.csv.gz`; `CALENDAR_FILE` is the absolute path of the reviewed JSON catalog. If recovery requires an idempotent data/catalog bootstrap or the demo account must be reset, run the applicable command explicitly:
```bash ```bash
docker compose --profile import run --rm importer docker compose --profile import run --rm importer
docker compose --profile tools run --rm calendar
docker compose --profile tools run --rm admin docker compose --profile tools run --rm admin
``` ```
Both commands are explicit one-offs. Re-running the importer relies on the backend's checksum-based idempotency guard. All three commands are explicit one-offs. Calendar bootstrap resumes the same reviewed version safely and rejects placeholder checksums, conflicting metadata, retired versions, or replacement of a different active catalog. Future catalog rotation must use the separately reviewed import, approve, and activate workflow.
## Operations ## Operations

View File

@@ -11,7 +11,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api ./c
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/worker ./cmd/worker \ && CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/worker ./cmd/worker \
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/migrate ./cmd/migrate \ && CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/migrate ./cmd/migrate \
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/importer ./cmd/importer \ && CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/importer ./cmd/importer \
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/admin ./cmd/admin && CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/admin ./cmd/admin \
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/calendar ./cmd/calendar
FROM alpine:3.22 FROM alpine:3.22

View File

@@ -95,15 +95,16 @@ If issuance fails, stop repeated production requests, inspect DNS/CAA/firewall/c
1. Acquire the production deployment lock and record the operator/release id. 1. Acquire the production deployment lock and record the operator/release id.
2. Place the approved `challenge_data.csv.gz` at the exact Git-ignored deployment `data/incoming/` path. Verify regular-file/read-only properties, size, and pinned SHA-256; do not copy it into an image. 2. Place the approved `challenge_data.csv.gz` at the exact Git-ignored deployment `data/incoming/` path. Verify regular-file/read-only properties, size, and pinned SHA-256; do not copy it into an image.
3. Invoke tracked `scripts/first-start.sh`. It starts PostgreSQL and Redis, waits with bounded backoff, applies the one-off migration, runs the one-shot importer with a read-only mount, and reconciles the pinned counts before starting public services. 3. Place the independently reviewed calendar JSON at the absolute `CALENDAR_FILE` path. Verify it is a regular read-only file, record its checksum and reviewer, and reject candidate data whose `sourceChecksum` still contains a review placeholder.
4. Review the JSON/Markdown import report and durable import-run/issues metadata. Fatal validation, resource, migration, or reconciliation failure must exit nonzero before backend/frontend/Nginx activation. 4. Invoke tracked `scripts/first-start.sh`. It starts PostgreSQL and Redis, waits with bounded backoff, applies the one-off migration, runs the one-shot importer and reconciliation, bootstraps the reviewed calendar through its audited `DRAFT`/`APPROVED`/`ACTIVE` states, and only then starts public services.
5. Verify backend and worker liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports. 5. Review the JSON/Markdown import report, durable import-run/issues metadata, active calendar version, and calendar audit records. Fatal validation, resource, migration, import, reconciliation, calendar, or scheduling failure must exit nonzero before backend/frontend/Nginx activation.
6. Verify the frontend static sentinel plus the emitted Iran Yekan font resource. The source font/license remain frontend-owned; no deployment-side injection or copy occurs. 6. Verify backend and worker liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports.
7. Verify Nginx public generic health, canonical redirect, security headers, login page, SSE behavior, and that only 80/443 are public. 7. Verify the frontend static sentinel plus the emitted Iran Yekan font resource. The source font/license remain frontend-owned; no deployment-side injection or copy occurs.
8. Run demo credential generation as a separate explicit administrative job. Store one-time plaintext output outside Git/logs/backups. 8. Verify Nginx public generic health, canonical redirect, security headers, login page, SSE behavior, and that only 80/443 are public.
9. Authenticate as at least two merchant accounts, verify different dashboards and cross-tenant isolation, then remove or escrow plaintext credential output. 9. Run demo credential generation as a separate explicit administrative job. Store one-time plaintext output outside Git/logs/backups.
10. Verify MVP operator checks: Compose health/restarts, structured logs, disk, certificate, queue/Redis state, and backup destination access. 10. Authenticate as at least two merchant accounts, verify different dashboards and cross-tenant isolation, then remove or escrow plaintext credential output.
11. Record the active three-hash release only after all postflight checks pass. 11. Verify MVP operator checks: Compose health/restarts, structured logs, disk, certificate, queue/Redis state, and backup destination access.
12. Record the active three-hash release only after all postflight checks pass.
Failure before the active-release switch restores the prior/empty known state. Do not improvise a database down migration. Failure before the active-release switch restores the prior/empty known state. Do not improvise a database down migration.