Revert "Restore staging payment and package management"
Staging Backend Quality Gate / validate (pull_request) Has been cancelled
Staging Backend Quality Gate / publish (pull_request) Has been cancelled

This reverts commit 751dee39dc, reversing
changes made to 43581ac0f7.
This commit is contained in:
linmeng
2026-07-30 10:10:17 +08:00
parent 751dee39dc
commit 96d0e250eb
185 changed files with 2185 additions and 12986 deletions
+3 -2
View File
@@ -3,19 +3,20 @@
APP_ENV_FILE=../.env.staging
CADDYFILE_PATH=./Caddyfile.staging
SITE_ADDRESS=https://staging.jyotisha.chat
ADMIN_SITE_ADDRESS=https://admin.staging.jyotisha.chat
# Staging-only cutover: identity and business data both use the private local
# PostgreSQL service. Production remains on Supabase until a separate cutover.
AUTH_PROVIDER=self-hosted
SELF_HOSTED_IDENTITY_ENABLED=true
AUTH_USER_ORIGIN=https://staging.jyotisha.chat
AUTH_ADMIN_ORIGIN=https://admin.staging.jyotisha.chat
IDENTITY_DATABASE_URL=postgresql://identity_runtime:<percent-encoded-identity-runtime-password>@postgres:5432/jyotisha
APP_DATABASE_URL=postgresql://app_runtime:<percent-encoded-app-runtime-password>@postgres:5432/jyotisha
ADMIN_DATABASE_URL=postgresql://admin_runtime:<percent-encoded-admin-runtime-password>@postgres:5432/jyotisha
BETTER_AUTH_USER_SECRET=<independent-openssl-rand-base64-32-output>
BETTER_AUTH_ADMIN_SECRET=<different-openssl-rand-base64-32-output>
RESEND_API_KEY=<staging-only-resend-api-key>
RESEND_FROM_EMAIL=Jyotisha Staging <login@staging.jyotisha.chat>
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>
+17
View File
@@ -1,4 +1,21 @@
{$SITE_ADDRESS:https://staging.jyotisha.chat} {
encode zstd gzip
@adminPaths path /admin /admin/* /api/admin/*
respond @adminPaths "Not found" 404
reverse_proxy web:3000
}
{$ADMIN_SITE_ADDRESS:https://admin.staging.jyotisha.chat} {
encode zstd gzip
@adminRoot path /
redir @adminRoot /admin/codes 302
@adminSurface path /login /admin /admin/* /api/admin/* /api/auth/* /_next/* /jyotish-logo.png /favicon.ico
handle @adminSurface {
reverse_proxy web:3000
}
respond "Not found" 404
}
+2 -11
View File
@@ -73,14 +73,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
ADMIN_EMAILS=...
# Required to save/read database-backed 易支付 settings. Base64 decoding must
# produce exactly 32 random bytes. Generate independently; never reuse auth keys.
EPAY_CONFIG_ENCRYPTION_KEY=<independent-base64-encoded-32-byte-key>
# Legacy EPAY_GATEWAY_URL / EPAY_PID / EPAY_KEY / EPAY_NOTIFY_URL /
# EPAY_RETURN_URL / EPAY_SITE_NAME remain fallback-only when no database row exists.
# Online packages stay hidden by default; only explicit true enables the fallback.
EPAY_CHAT_ENABLED=false
# Conversational birth-time rectification rollout controls.
# Keep migrations false until the ordered database gate below has passed.
RECTIFICATION_PRICE_CREDITS=3
@@ -184,9 +176,10 @@ The staging env file must include these non-secret selectors so Compose cannot f
APP_ENV_FILE=../.env.staging
CADDYFILE_PATH=./Caddyfile.staging
SITE_ADDRESS=https://staging.jyotisha.chat
ADMIN_SITE_ADDRESS=https://admin.staging.jyotisha.chat
```
Staging is fully self-hosted: set `AUTH_PROVIDER=self-hosted` and `SELF_HOSTED_IDENTITY_ENABLED=true`. Add the three role-specific server-only database URLs, the single `AUTH_USER_ORIGIN` and `BETTER_AUTH_USER_SECRET`, and staging-only Resend settings listed in `deploy/.env.staging.identity.example`. The main-site Better Auth user session is also used by `/admin`; persisted `identity.users.role=admin` is the only self-hosted backend role, while `viewer` and ordinary users are denied. Browser code uses same-origin APIs; it receives neither database credentials nor Supabase keys. Production remains on Supabase and is not changed by the staging workflow. See `docs/operations/self-hosted-identity.md` for validation and rollback commands.
Staging is fully self-hosted: set `AUTH_PROVIDER=self-hosted` and `SELF_HOSTED_IDENTITY_ENABLED=true`. Add the three role-specific server-only database URLs, separate user/admin Better Auth secrets, origins, and staging-only Resend settings listed in `deploy/.env.staging.identity.example`. Browser code uses same-origin APIs; it receives neither database credentials nor Supabase keys. Production remains on Supabase and is not changed by the staging workflow. See `docs/operations/self-hosted-identity.md` for validation and rollback commands.
After source sync and before `up`, the workflow validates `.env.staging` mode/selectors, explicitly pins the three staging selectors against ambient shell overrides, and runs `docker compose --env-file .env.staging -f deploy/docker-compose.server.yml config --quiet`. For later manual inspections, run the same checks only after the tracked deployment files exist on the server. Do not use a manual gate run from `main` as the first publishing path: publishing requires a successful push to `staging`, while manual `Deploy staging` requires a successful gate run for the exact SHA.
@@ -200,8 +193,6 @@ After source sync and before `up`, the workflow validates `.env.staging` mode/se
6. If the read-only checker reports a pending migration, stop app deployment and run `Migrate Staging Database` manually with the same full SHA; a successful migration re-dispatches `Deploy staging` with that same SHA.
7. Confirm `https://staging.jyotisha.chat/api/health` reports the exact SHA and private API health.
After the exact-SHA deployment and migrations are verified, use the manual `Configure Staging Rectification Rollout` workflow to change new-case creation. Supply the SHA currently reported by `/api/health`; choose `public` to open all staging accounts, `smoke_only` with canonical test-account UUIDs for a canary, or `paused` to close creation. The workflow updates only the four `RECTIFICATION_V3_*` rollout variables under the shared host lock, recreates `web` and `rectification-v4-worker` with the already deployed image, and rolls back the env file if health does not match the requested audience. Do not edit or print `.env.staging` through CI logs.
Application rollback uses the same workflow: manually dispatch `Deploy staging` from the `main` controller with a previous known-good full SHA that has a successful `Staging Backend Quality Gate` run, and explicitly set `allow_rollback=true`. Normal and migration-triggered deployments reject stale, divergent, or backward revisions. Rollback still consumes the selected gate run's digest manifest and is supported only during that artifact's 30-day retention window; after expiry, stop and prepare a separately reviewed republish/recovery change rather than substituting a mutable tag or assuming the old run can still be rerun. Database migrations are separate and are not rolled back by an application deployment. Restore a staging database backup before running any destructive migration rehearsal.
Inspect staging without printing secrets:
@@ -1,166 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
set +x
required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA ROLLOUT_AUDIENCE STAGING_URL)
for key in "${required[@]}"; do
if [ -z "${!key:-}" ]; then
echo "required staging rollout input is missing: $key" >&2
exit 1
fi
done
sha_pattern='^[0-9a-f]{40}$'
uuid_pattern='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
[[ "$EXPECTED_DEPLOY_SHA" =~ $sha_pattern ]] || {
echo "invalid expected deployment SHA" >&2
exit 1
}
case "$ROLLOUT_AUDIENCE" in
paused|smoke_only|public) ;;
*) echo "invalid rollout audience" >&2; exit 1 ;;
esac
smoke_user_ids="${SYNTHETIC_SMOKE_USER_IDS:-}"
if [ "$ROLLOUT_AUDIENCE" = "smoke_only" ]; then
[ -n "$smoke_user_ids" ] || {
echo "smoke_only requires at least one synthetic user UUID" >&2
exit 1
}
IFS=',' read -ra smoke_users <<<"$smoke_user_ids"
for user_id in "${smoke_users[@]}"; do
[[ "$user_id" =~ $uuid_pattern ]] || {
echo "invalid synthetic smoke user UUID" >&2
exit 1
}
done
else
[ -z "$smoke_user_ids" ] || {
echo "synthetic smoke users are only valid for smoke_only" >&2
exit 1
}
fi
state_directory="$DEPLOY_PATH/.state"
env_file="$DEPLOY_PATH/.env.staging"
install -d -m 700 "$state_directory"
exec 9>"$state_directory/mutation.lock"
flock -n 9 || {
echo "another staging mutation holds the host lock" >&2
exit 75
}
compose_files=(
-f deploy/docker-compose.server.yml
-f deploy/docker-compose.postgres.yml
-f deploy/docker-compose.staging.yml
)
[ -f "$env_file" ] || {
echo "staging environment file is missing" >&2
exit 1
}
current_sha="$(<"$state_directory/deployed-revision")"
[ "$current_sha" = "$EXPECTED_DEPLOY_SHA" ] || {
echo "deployed staging revision does not match the approved rollout SHA" >&2
exit 1
}
case "$ROLLOUT_AUDIENCE" in
public)
creation_enabled=true
smoke_sha="$EXPECTED_DEPLOY_SHA"
smoke_user_ids=""
;;
smoke_only)
creation_enabled=true
smoke_sha=""
;;
paused)
creation_enabled=false
smoke_sha=""
smoke_user_ids=""
;;
esac
backup="$(mktemp "$state_directory/rectification-rollout-backup.XXXXXX")"
temporary="$(mktemp "$DEPLOY_PATH/.env.staging.rollout.XXXXXX")"
declare -a compose=()
cleanup() { rm -f -- "$backup" "$temporary"; }
rollback() {
local status=$?
cp -p -- "$backup" "$env_file"
if [ "${#compose[@]}" -gt 0 ]; then
"${compose[@]}" up -d --no-build --pull never --force-recreate --no-deps web rectification-v4-worker >/dev/null 2>&1 || true
fi
exit "$status"
}
trap cleanup EXIT
cp -p -- "$env_file" "$backup"
awk \
-v create="$creation_enabled" \
-v migrations="true" \
-v smoke_sha="$smoke_sha" \
-v smoke_users="$smoke_user_ids" '
BEGIN {
values["RECTIFICATION_V3_CREATE_ENABLED"] = create
values["RECTIFICATION_V3_MIGRATIONS_READY"] = migrations
values["RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA"] = smoke_sha
values["RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS"] = smoke_users
}
{
split($0, parts, "=")
key = parts[1]
if (key in values) {
if (!(key in written)) print key "=" values[key]
written[key] = 1
next
}
print
}
END {
for (key in values) if (!(key in written)) print key "=" values[key]
}
' "$env_file" >"$temporary"
chmod 600 "$temporary"
cd "$DEPLOY_PATH"
bash deploy/validate-staging-env.sh "$temporary" staging.jyotisha.chat deploy/Caddyfile.staging
mv -f -- "$temporary" "$env_file"
trap rollback ERR
web_container="$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1)"
[ -n "$web_container" ] || {
echo "staging web container is missing" >&2
false
}
export WEB_IMAGE="$(docker inspect --format '{{.Config.Image}}' "$web_container")"
export APP_ENV_FILE='../.env.staging'
export DATABASE_ENV_FILE='../.env.staging.database'
export CADDYFILE_PATH='./Caddyfile.staging'
export SITE_ADDRESS='https://staging.jyotisha.chat'
export GITHUB_SHA="$EXPECTED_DEPLOY_SHA"
compose=(docker compose -p jyotisha-staging --env-file .env.staging "${compose_files[@]}")
"${compose[@]}" config --quiet
"${compose[@]}" up -d --no-build --pull never --force-recreate --no-deps web rectification-v4-worker
health=""
for _ in $(seq 1 30); do
health="$(curl --fail --silent --show-error "$STAGING_URL/api/health" 2>/dev/null || true)"
expected_ready=false
[ "$ROLLOUT_AUDIENCE" = public ] && expected_ready=true
if grep -Fq "\"gitCommit\":\"$EXPECTED_DEPLOY_SHA\"" <<<"$health" &&
grep -Fq "\"creationAudience\":\"$ROLLOUT_AUDIENCE\"" <<<"$health" &&
grep -Fq "\"readyForNewCases\":$expected_ready" <<<"$health"; then
trap - ERR
printf 'rectification rollout audience=%s deployed_sha=%s ready_for_new_cases=%s\n' \
"$ROLLOUT_AUDIENCE" "$EXPECTED_DEPLOY_SHA" "$([ "$ROLLOUT_AUDIENCE" = public ] && echo true || echo false)"
exit 0
fi
sleep 2
done
echo "staging rollout health verification failed" >&2
false
+1
View File
@@ -53,6 +53,7 @@ services:
restart: unless-stopped
environment:
SITE_ADDRESS: ${SITE_ADDRESS:-https://jyotisha.chat}
ADMIN_SITE_ADDRESS: ${ADMIN_SITE_ADDRESS:-https://admin.staging.jyotisha.chat}
ports:
- "80:80"
- "443:443"
+3 -6
View File
@@ -1,14 +1,11 @@
FROM m.daocloud.io/docker.io/library/python:3.12-slim
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ \
PIP_DEFAULT_TIMEOUT=60
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt ./
RUN sed -i 's|http://deb.debian.org|https://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 update \
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& python -m pip install -r requirements.txt \
&& apt-get purge -y --auto-remove build-essential \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-alpine
FROM node:22-alpine
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
-290
View File
@@ -1,290 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
set +x
required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA RESET_EMAIL RESET_CONFIRMATION)
for key in "${required[@]}"; do
if [ -z "${!key:-}" ]; then
echo "required staging account-reset input is missing: $key" >&2
exit 1
fi
done
[[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || {
echo "invalid expected deployment SHA" >&2
exit 1
}
[[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]] || {
echo "invalid reset email" >&2
exit 1
}
[ "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL" ] || {
echo "account reset confirmation does not match" >&2
exit 1
}
[ "$DEPLOY_PATH" = "/opt/jyotisha-staging" ] || {
echo "refusing non-staging deployment path" >&2
exit 1
}
state_directory="$DEPLOY_PATH/.state"
[ -f "$state_directory/deployed-revision" ] || {
echo "staging deployed revision is unavailable" >&2
exit 1
}
[ "$(<"$state_directory/deployed-revision")" = "$EXPECTED_DEPLOY_SHA" ] || {
echo "deployed staging revision does not match the approved reset SHA" >&2
exit 1
}
install -d -m 700 "$state_directory"
exec 9>"$state_directory/mutation.lock"
flock -n 9 || {
echo "another staging mutation holds the host lock" >&2
exit 75
}
cd "$DEPLOY_PATH"
compose=(docker compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml)
"${compose[@]}" ps --status running postgres --quiet | grep -q . || {
echo "staging postgres container is not running" >&2
exit 1
}
run_psql() {
"${compose[@]}" exec -T -e RESET_EMAIL="$RESET_EMAIL" postgres sh -ceu '
exec psql -X -v ON_ERROR_STOP=1 -v target_email="$RESET_EMAIL" \
-U "$POSTGRES_USER" -d "$POSTGRES_DB"
'
}
run_psql <<'SQL'
begin;
create temporary table reset_snapshot on commit drop as
select
identity_user.id,
identity_user.email,
profile.email as profile_email,
profile.credits,
(select count(*) from identity.accounts value where value.user_id = identity_user.id) as identity_accounts,
(select count(*) from identity.sessions value where value.user_id = identity_user.id) as identity_sessions,
(select count(*) from public.credit_transactions value where value.user_id = identity_user.id) as credit_transactions,
(select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id) as credit_cancellations,
(select count(*) from public.consultation_requests value where value.user_id = identity_user.id) as consultation_requests,
(select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id) as rectification_billing,
(select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id) as action_receipts,
(select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id) as redeemed_codes,
(select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id) as admin_audit_logs
from identity.users identity_user
join auth.users auth_user on auth_user.id = identity_user.id
join public.profiles profile on profile.id = identity_user.id
where lower(btrim(identity_user.email)) = lower(btrim(:'target_email'))
and lower(btrim(auth_user.email)) = lower(btrim(:'target_email'))
for update of identity_user, auth_user, profile;
do $$
begin
if (select count(*) from reset_snapshot) <> 1 then
raise exception 'account_not_found_or_identity_bridge_mismatch';
end if;
end $$;
select jsonb_build_object(
'stage', 'preflight',
'email', snapshot.email,
'credits', snapshot.credits,
'identityAccounts', snapshot.identity_accounts,
'identitySessions', snapshot.identity_sessions,
'creditTransactions', snapshot.credit_transactions,
'creditCancellations', snapshot.credit_cancellations,
'consultationRequests', snapshot.consultation_requests,
'rectificationBilling', snapshot.rectification_billing,
'actionReceipts', snapshot.action_receipts,
'redeemedCodes', snapshot.redeemed_codes,
'adminAuditLogs', snapshot.admin_audit_logs,
'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = snapshot.id),
'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = snapshot.id),
'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = snapshot.id),
'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = snapshot.id),
'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = snapshot.id),
'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = snapshot.id),
'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = snapshot.id),
'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = snapshot.id)
)
from reset_snapshot snapshot;
update public.profiles profile
set name = null,
birth_date = null,
birth_time = null,
country_code = null,
province_code = null,
city_code = null,
district_code = null,
onboarding_payload = null,
onboarding_version = null,
onboarding_generated_at = null,
latitude = null,
longitude = null,
timezone_offset = null,
reported_birth_time = null,
active_birth_time = null,
birth_time_source = null,
birth_time_period = null,
birth_time_clue = null,
uncertainty_before_minutes = null,
uncertainty_after_minutes = null,
birth_time_status = null,
rectification_confidence = null,
rectification_case_id = null,
birth_place_label = null,
birth_place_type = null,
birth_place_provider = null,
birth_place_provider_id = null,
timezone_id = null,
timezone_source = null,
updated_at = pg_catalog.now()
from reset_snapshot snapshot
where profile.id = snapshot.id;
delete from public.chat_sessions value using reset_snapshot snapshot where value.user_id = snapshot.id;
delete from public.chart_profiles value using reset_snapshot snapshot where value.user_id = snapshot.id;
delete from public.synastry_reports value using reset_snapshot snapshot where value.user_id = snapshot.id;
delete from public.birth_time_rectification_v4_cases value using reset_snapshot snapshot where value.user_id = snapshot.id;
delete from public.birth_time_rectification_cases value using reset_snapshot snapshot where value.user_id = snapshot.id;
do $$
begin
if exists (
select 1
from reset_snapshot snapshot
join identity.users identity_user on identity_user.id = snapshot.id
join auth.users auth_user on auth_user.id = snapshot.id
join public.profiles profile on profile.id = snapshot.id
where identity_user.email is distinct from snapshot.email
or auth_user.email is distinct from snapshot.email
or profile.email is distinct from snapshot.profile_email
or profile.credits is distinct from snapshot.credits
or (select count(*) from identity.accounts value where value.user_id = snapshot.id) <> snapshot.identity_accounts
or (select count(*) from identity.sessions value where value.user_id = snapshot.id) <> snapshot.identity_sessions
or (select count(*) from public.credit_transactions value where value.user_id = snapshot.id) <> snapshot.credit_transactions
or (select count(*) from public.credit_request_cancellations value where value.user_id = snapshot.id) <> snapshot.credit_cancellations
or (select count(*) from public.consultation_requests value where value.user_id = snapshot.id) <> snapshot.consultation_requests
or (select count(*) from public.birth_time_rectification_billing value where value.user_id = snapshot.id) <> snapshot.rectification_billing
or (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = snapshot.id) <> snapshot.action_receipts
or (select count(*) from public.redemption_codes value where value.redeemed_by = snapshot.id) <> snapshot.redeemed_codes
or (select count(*) from audit.admin_audit_logs value where value.actor_user_id = snapshot.id) <> snapshot.admin_audit_logs
) then
raise exception 'preserved_state_changed';
end if;
if exists (select 1 from public.chat_sessions value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.chart_profiles value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.synastry_reports value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_cases value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_v4_cases value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_v4_jobs value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_agent_runs value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_diagnostics value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_public_messages value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.birth_time_rectification_pending_evidence value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (
select 1 from public.profiles profile join reset_snapshot snapshot on profile.id = snapshot.id
where profile.name is not null or profile.birth_date is not null or profile.birth_time is not null
or profile.country_code is not null or profile.province_code is not null or profile.city_code is not null or profile.district_code is not null
or profile.onboarding_payload is not null or profile.onboarding_version is not null or profile.onboarding_generated_at is not null
or profile.latitude is not null or profile.longitude is not null or profile.timezone_offset is not null
or profile.reported_birth_time is not null or profile.active_birth_time is not null or profile.birth_time_source is not null
or profile.birth_time_period is not null or profile.birth_time_clue is not null
or profile.uncertainty_before_minutes is not null or profile.uncertainty_after_minutes is not null
or profile.birth_time_status is not null or profile.rectification_confidence is not null or profile.rectification_case_id is not null
or profile.birth_place_label is not null or profile.birth_place_type is not null or profile.birth_place_provider is not null
or profile.birth_place_provider_id is not null or profile.timezone_id is not null or profile.timezone_source is not null
) then
raise exception 'reset_state_not_empty';
end if;
end $$;
commit;
SQL
run_psql <<'SQL'
begin;
create temporary table postflight_target on commit drop as
select identity_user.id, identity_user.email, profile.credits,
not (
profile.name is null and profile.birth_date is null and profile.birth_time is null
and profile.country_code is null and profile.province_code is null and profile.city_code is null and profile.district_code is null
and profile.onboarding_payload is null and profile.onboarding_version is null and profile.onboarding_generated_at is null
and profile.latitude is null and profile.longitude is null and profile.timezone_offset is null
and profile.reported_birth_time is null and profile.active_birth_time is null and profile.birth_time_source is null
and profile.birth_time_period is null and profile.birth_time_clue is null
and profile.uncertainty_before_minutes is null and profile.uncertainty_after_minutes is null
and profile.birth_time_status is null and profile.rectification_confidence is null and profile.rectification_case_id is null
and profile.birth_place_label is null and profile.birth_place_type is null and profile.birth_place_provider is null
and profile.birth_place_provider_id is null and profile.timezone_id is null and profile.timezone_source is null
) as profile_not_reset,
lower(btrim(profile.email)) = lower(btrim(identity_user.email)) as profile_email_matches
from identity.users identity_user
join auth.users auth_user on auth_user.id = identity_user.id
and lower(btrim(auth_user.email)) = lower(btrim(identity_user.email))
join public.profiles profile on profile.id = identity_user.id
where lower(btrim(identity_user.email)) = lower(btrim(:'target_email'));
do $$
declare
target_id uuid;
begin
if (select count(*) from postflight_target) <> 1 then
raise exception 'postflight_account_not_found_or_identity_bridge_mismatch';
end if;
select id into target_id from postflight_target;
if exists (select 1 from public.chat_sessions value where value.user_id = target_id)
or exists (select 1 from public.chart_profiles value where value.user_id = target_id)
or exists (select 1 from public.synastry_reports value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_cases value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_v4_cases value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_v4_jobs value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_agent_runs value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_diagnostics value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_public_messages value where value.user_id = target_id)
or exists (select 1 from public.birth_time_rectification_pending_evidence value where value.user_id = target_id)
or exists (select 1 from postflight_target where profile_not_reset) then
raise exception 'postflight_reset_state_not_empty';
end if;
end $$;
select jsonb_build_object(
'stage', 'postflight',
'matchedAccounts', (select count(*) from postflight_target),
'email', (select email from postflight_target),
'credits', (select credits from postflight_target),
'profileEmailMatches', (select profile_email_matches from postflight_target),
'profileNotReset', (select profile_not_reset from postflight_target),
'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = (select id from postflight_target)),
'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = (select id from postflight_target)),
'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = (select id from postflight_target)),
'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = (select id from postflight_target)),
'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = (select id from postflight_target)),
'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = (select id from postflight_target)),
'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = (select id from postflight_target)),
'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = (select id from postflight_target)),
'v5FeatureSnapshots', (select count(*) from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = (select id from postflight_target)),
'v5PublicMessages', (select count(*) from public.birth_time_rectification_public_messages value where value.user_id = (select id from postflight_target)),
'v5PendingEvidence', (select count(*) from public.birth_time_rectification_pending_evidence value where value.user_id = (select id from postflight_target)),
'identityAccounts', (select count(*) from identity.accounts value where value.user_id = (select id from postflight_target)),
'identitySessions', (select count(*) from identity.sessions value where value.user_id = (select id from postflight_target)),
'creditTransactions', (select count(*) from public.credit_transactions value where value.user_id = (select id from postflight_target)),
'creditCancellations', (select count(*) from public.credit_request_cancellations value where value.user_id = (select id from postflight_target)),
'consultationRequests', (select count(*) from public.consultation_requests value where value.user_id = (select id from postflight_target)),
'rectificationBilling', (select count(*) from public.birth_time_rectification_billing value where value.user_id = (select id from postflight_target)),
'actionReceipts', (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = (select id from postflight_target))
);
commit;
SQL
+14 -9
View File
@@ -14,7 +14,7 @@ for key in "${required[@]}"; do
done
sha_pattern='^[0-9a-f]{40}$'
digest_pattern='^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[1-9][0-9]{0,4})?(/[a-z0-9]+([._-][a-z0-9]+)*)+@sha256:[0-9a-f]{64}$'
digest_pattern='^ghcr\.io/jesse-ux/jyotisha-(api|web)@sha256:[0-9a-f]{64}$'
image_id_pattern='^sha256:[0-9a-f]{64}$'
if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] ||
[[ ! "$API_IMAGE" =~ $digest_pattern ]] ||
@@ -22,8 +22,6 @@ if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] ||
echo "unsafe staging image identity" >&2
exit 1
fi
api_repository="${API_IMAGE%@sha256:*}"
web_repository="${WEB_IMAGE%@sha256:*}"
if [ "$ALLOW_ROLLBACK" != "true" ] && [ "$ALLOW_ROLLBACK" != "false" ]; then
echo "invalid rollback authorization" >&2
exit 1
@@ -87,8 +85,8 @@ repo_digest_for_container() {
awk -v prefix="$repository@sha256:" 'index($0, prefix) == 1 { print; exit }'
}
previous_api_image="$(repo_digest_for_container api "$api_repository")"
previous_web_image="$(repo_digest_for_container web "$web_repository")"
previous_api_image="$(repo_digest_for_container api ghcr.io/jesse-ux/jyotisha-api)"
previous_web_image="$(repo_digest_for_container web ghcr.io/jesse-ux/jyotisha-web)"
previous_api_id=""
previous_web_id=""
if [ -n "$(container_id api)" ]; then
@@ -128,6 +126,7 @@ export APP_ENV_FILE='../.env.staging'
export DATABASE_ENV_FILE='../.env.staging.database'
export CADDYFILE_PATH='./Caddyfile.staging'
export SITE_ADDRESS='https://staging.jyotisha.chat'
export ADMIN_SITE_ADDRESS='https://admin.staging.jyotisha.chat'
export GITHUB_SHA="$DEPLOY_SHA"
"${compose[@]}" config --quiet
@@ -185,6 +184,7 @@ verify_container_image rectification-v4-worker "$WEB_IMAGE"
"${compose[@]}" exec -T \
-e EXPECTED_SHA="$DEPLOY_SHA" -e STAGING_URL="$STAGING_URL" \
-e STAGING_ADMIN_URL="https://admin.staging.jyotisha.chat" \
web node --input-type=module <<'NODE'
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let login;
@@ -196,10 +196,15 @@ for (let attempt = 0; attempt < 12; attempt += 1) {
await delay(5_000);
}
if (!login?.ok) process.exit(1);
const adminPage = await fetch(`${process.env.STAGING_URL}/admin`, { redirect: "manual" });
if (adminPage.status !== 307 || adminPage.headers.get("location") !== "/login") process.exit(1);
const adminApi = await fetch(`${process.env.STAGING_URL}/api/admin/session`);
if (adminApi.status !== 401) process.exit(1);
const adminLogin = await fetch(`${process.env.STAGING_ADMIN_URL}/login`);
if (!adminLogin.ok) process.exit(1);
const adminRoot = await fetch(process.env.STAGING_ADMIN_URL, { redirect: "manual" });
if (
adminRoot.status !== 302 ||
adminRoot.headers.get("location") !== "/admin/codes"
) process.exit(1);
const adminSession = await fetch(`${process.env.STAGING_ADMIN_URL}/api/auth/get-session`);
if (!adminSession.ok) process.exit(1);
const account = await fetch(`${process.env.STAGING_URL}/api/account`);
if (account.status !== 401) process.exit(1);
const publicHealth = await fetch(`${process.env.STAGING_URL}/api/health`);
+1 -2
View File
@@ -17,8 +17,7 @@ done
echo "unsafe staging migration revision" >&2
exit 1
}
image_pattern='^crpi-d1feco6itet73spp\.cn-hongkong\.personal\.cr\.aliyuncs\.com/copse/jyotisha@sha256:[0-9a-f]{64}$'
[[ "$WEB_IMAGE" =~ $image_pattern ]] || {
[[ "$WEB_IMAGE" =~ ^ghcr\.io/jesse-ux/jyotisha-web@sha256:[0-9a-f]{64}$ ]] || {
echo "unsafe staging migration image" >&2
exit 1
}
+9 -7
View File
@@ -41,9 +41,11 @@ require_selector() {
require_selector APP_ENV_FILE ../.env.staging
require_selector CADDYFILE_PATH ./Caddyfile.staging
require_selector SITE_ADDRESS https://staging.jyotisha.chat
require_selector ADMIN_SITE_ADDRESS https://admin.staging.jyotisha.chat
require_selector AUTH_PROVIDER self-hosted
require_selector SELF_HOSTED_IDENTITY_ENABLED true
require_selector AUTH_USER_ORIGIN https://staging.jyotisha.chat
require_selector AUTH_ADMIN_ORIGIN https://admin.staging.jyotisha.chat
require_literal() {
local key="$1"
@@ -86,6 +88,13 @@ if ! [[ "$admin_database_url" =~ ^postgresql://admin_runtime:([A-Za-z0-9._~-]|%[
fi
require_literal BETTER_AUTH_USER_SECRET 32
user_secret="$LITERAL_VALUE"
require_literal BETTER_AUTH_ADMIN_SECRET 32
admin_secret="$LITERAL_VALUE"
if [ "$user_secret" = "$admin_secret" ]; then
echo "staging identity secrets must be different" >&2
exit 1
fi
require_literal RESEND_API_KEY 10
require_literal RESEND_FROM_EMAIL 5
if [[ "$LITERAL_VALUE" != *@* ]]; then
@@ -97,13 +106,6 @@ if [[ "$LITERAL_VALUE" != *@* ]]; then
echo "invalid staging identity setting: ADMIN_EMAILS" >&2
exit 1
fi
require_literal EPAY_CONFIG_ENCRYPTION_KEY 44
if [ "${#LITERAL_VALUE}" -ne 44 ] ||
[[ ! "$LITERAL_VALUE" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
echo "invalid staging identity setting: EPAY_CONFIG_ENCRYPTION_KEY" >&2
exit 1
fi
require_selector EPAY_CHAT_ENABLED false
require_literal JYOTISH_DYNAMIC_RECTIFICATION_TOKEN 32
echo "staging environment selectors: valid"