Revert "Restore staging payment and package management"
This reverts commit751dee39dc, reversing changes made to43581ac0f7.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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 ./
|
||||
|
||||
@@ -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,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`);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user