Merge GitHub upstream into Gitea primary

This commit is contained in:
Jesse_Chen
2026-08-05 11:13:34 +08:00
105 changed files with 3612 additions and 886 deletions
+2 -3
View File
@@ -3,20 +3,19 @@
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,21 +1,4 @@
{$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
}
+21 -13
View File
@@ -14,7 +14,8 @@ This file is the operational source of truth for the current Jyotisha demo deplo
| Capacity | 1 vCPU / 2 GB RAM / 40 GB disk / 5 Mbps |
| App directory | `/opt/jyotisha-app` |
| Environment file | `/opt/jyotisha-app/.env.production` (`0600`) |
| Source repository | `https://github.com/jesse-ux/Jyotisha.git` |
| Primary source repository | `https://git.copse.top/root/Jyotisha.git` |
| GitHub upstream/mirror | `https://github.com/jesse-ux/Jyotisha.git` |
| Supabase project | `vtvnfqmonbfuxmqkqdlc` |
This machine is suitable for a client demo and low concurrency. Supabase and the model provider stay managed externally; do not self-host them on this VPS.
@@ -73,6 +74,14 @@ 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
@@ -164,11 +173,11 @@ Staging is isolated from production:
| PostgreSQL | private Compose network; no published host port |
| Business database | local private PostgreSQL (`jyotisha-staging` Compose project) |
| Identity | Better Auth + Resend OTP on the same private PostgreSQL cluster |
| GitHub Environment | `staging` |
| Actions control plane | Gitea 1.26.2 (`git.copse.top`) |
The GitHub `staging` Environment contains the secret `STAGING_SSH_PRIVATE_KEY` and the variables `STAGING_HOST`, `STAGING_PORT`, `STAGING_USER`, `STAGING_PATH`, `STAGING_URL`, and `STAGING_KNOWN_HOSTS`. Its deployment branch policy allows the `main` controller branch: GitHub's `workflow_run` event executes from the default branch while the workflow separately requires the successfully tested upstream branch to be `staging`. The controller checks out only `main` with full history, requires the requested staging SHA to be an ancestor of that reviewed history, and uploads only the allowlisted `deploy/` control files. It never executes deployment validators or remote orchestration scripts from the target/rollback revision. The staging key, database, Resend key, and model-provider keys must not be shared with production. Staging image publishing has no Supabase build variables.
Gitea is the primary source repository and Actions control plane. Gitea automatically injects the per-job `${{ secrets.GITEA_TOKEN }}` token; its access is limited by each workflow's `permissions` block and it must not be configured as a repository secret. Configure repository Actions secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `STAGING_SSH_PRIVATE_KEY`, plus variables `STAGING_HOST`, `STAGING_PORT`, `STAGING_USER`, `STAGING_PATH`, `STAGING_URL`, and `STAGING_KNOWN_HOSTS`. The `workflow_run` controller is loaded from the default `main` branch while separately requiring the successfully tested upstream branch to be `staging`. The controller checks out only `main` with full history, requires the requested staging SHA to be an ancestor of that reviewed history, and uploads only the allowlisted `deploy/` control files. It never executes deployment validators or remote orchestration scripts from the target/rollback revision. The staging key, database, Resend key, and model-provider keys must not be shared with production. Staging image publishing has no Supabase build variables. GitHub workflows are upstream/mirror fallback only, not the normal staging release path.
`Staging Backend Quality Gate` runs for relevant `pull_request` paths, pushes to `staging`, and `workflow_dispatch`. It validates the Python/database/frontend contract; only a successful push to `staging` publishes the API/web images and a run-bound manifest containing their `sha256` digests. `.github/workflows/deploy-staging.yml` consumes that exact successful run, validates its manifest against the full 40-character commit, and deploys digest references rather than trusting the discoverability tags.
`Staging Backend Quality Gate` runs for relevant `pull_request` paths, pushes to `staging`, and `workflow_dispatch`. It validates the Python/database/frontend contract; only a successful push to `staging` publishes the API/web images and a run-bound manifest containing their `sha256` digests. `.gitea/workflows/deploy-staging.yml` consumes that exact successful run, validates its manifest against the full 40-character commit, and deploys digest references rather than trusting the discoverability tags.
The staging env file must include these non-secret selectors so Compose cannot fall back to production paths:
@@ -176,22 +185,21 @@ 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, 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.
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.
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.
### First-deploy sequence
1. Complete the server and GitHub bootstrap: create both mode-`0600` env files, preload the reviewed `postgres:17-alpine` image, and configure the staging Environment variables/secrets. No repository-level Supabase variables are required. Deployment and migration workflows use `--pull never` for PostgreSQL, so database image upgrades remain an explicit operator-controlled maintenance action rather than an application-deploy side effect.
2. Merge the reviewed change to `main`, then fast-forward/push that exact reviewed SHA to `staging`; do not create a staging-only target or rely on a `main` workflow dispatch to publish images.
1. Complete the server and Gitea bootstrap: create both mode-`0600` env files, preload the reviewed `postgres:17-alpine` image, and configure the listed Actions variables/secrets. No repository-level Supabase variables are required. Deployment and migration workflows use `--pull never` for PostgreSQL, so database image upgrades remain an explicit operator-controlled maintenance action rather than an application-deploy side effect.
2. Open a PR and merge the reviewed change to `main`, then fast-forward/push that same exact SHA to `staging`; do not create a staging-only target or rely on a `main` workflow dispatch to publish images.
3. The `Staging Backend Quality Gate` runs for that push and, when successful, publishes API/web images plus an artifact binding the exact SHA to both immutable image digests.
4. The automatic `Deploy staging` workflow downloads that gate-run artifact, syncs only the trusted `main` controller's allowlisted `deploy/` files under the shared staging host lock, and validates both `.env.staging` and `.env.staging.database` before any app change. The target application's code is carried only by the digest-pinned images.
4. The automatic `Deploy staging` workflow downloads that gate-run artifact, syncs only the trusted default-`main` controller's allowlisted `deploy/` files under the shared staging host lock, and validates both `.env.staging` and `.env.staging.database` before any app change. The target application's code is carried only by the digest-pinned images.
5. If environment validation fails, fix the server-side env files without committing or copying secrets, then manually rerun `Deploy staging` from `main` with the same successful SHA in `deploy_sha`; the workflow rechecks a successful staging gate for that exact SHA.
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.
6. If the read-only checker reports a pending migration, stop app deployment and run `Migrate Staging Database` manually with the same full SHA. Migration success does not dispatch deployment.
7. After migration succeeds, the operator must manually start `Deploy staging` from `main` with that same exact SHA, then confirm `https://staging.jyotisha.chat/api/health` reports it 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.
@@ -252,11 +260,11 @@ PostgreSQL is private: `deploy/docker-compose.postgres.yml` has no `ports` mappi
Use this order for every staging revision:
1. Merge the reviewed revision to `main`, then fast-forward/push that same exact SHA to `staging`.
1. Open a PR and merge the reviewed revision to `main`, then fast-forward/push that same exact SHA to `staging`.
2. Wait for `Staging Backend Quality Gate` to pass and publish that exact full SHA's API/web digest manifest.
3. The automatic `Deploy staging` workflow checks the exact SHA in read-only migration-check mode before changing API, web, or Caddy. If it reports pending or drifted migrations, stop; do not retry the application deployment as if it were a migration.
4. Open **Migrate Staging Database -> Run workflow**, select **Use workflow from: main**, and enter the reported full lowercase 40-character SHA in `deploy_sha`. The controller validates that exact SHA against a successful `staging` gate and reviewed `main` history, starts only PostgreSQL, and runs the digest-pinned migrator without executing scripts from the target revision.
5. A successful migration rechecks that `staging` still points at the same exact SHA, prints the ordered migration ledger, and dispatches the `main` controller for digest-pinned deployment with `allow_rollback=false`. If `staging` advanced during migration, it refuses the stale dispatch. Do not substitute a branch name, a short SHA, or a newer commit.
5. A successful migration rechecks that `staging` still points at the same exact SHA and prints the ordered migration ledger, but does not dispatch deployment. The operator must then open **Deploy staging -> Run workflow**, select **Use workflow from: main**, and enter the same exact SHA in `deploy_sha` with `allow_rollback=false`. If `staging` advanced, stop rather than substituting a branch name, short SHA, or newer commit.
6. Confirm `https://staging.jyotisha.chat/api/health` and verify that its deployment SHA is the SHA from step 2.
7. After health verification, create the local encrypted backup described below.
@@ -144,7 +144,6 @@ 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="$EXPECTED_DEPLOY_SHA"
compose=(docker compose -p jyotisha-staging --env-file .env.staging "${compose_files[@]}")
-1
View File
@@ -53,7 +53,6 @@ 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"
+6 -3
View File
@@ -1,11 +1,14 @@
FROM python:3.12-slim
FROM m.daocloud.io/docker.io/library/python:3.12-slim
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
PIP_NO_CACHE_DIR=1 \
PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ \
PIP_DEFAULT_TIMEOUT=60
WORKDIR /app
COPY requirements.txt ./
RUN apt-get update \
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 \
&& 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 node:22-alpine
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-alpine
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
+26 -26
View File
@@ -6,6 +6,11 @@ required=(
INCOMING_PATH DEPLOY_PATH API_IMAGE WEB_IMAGE DEPLOY_SHA
EXPECTED_PREVIOUS_SHA ALLOW_ROLLBACK DOCKER_CONFIG STAGING_URL
)
case "${DOCKER_BIN:-docker}" in
docker) docker_command=(docker) ;;
"sudo -n docker") docker_command=(sudo -n docker --config "$DOCKER_CONFIG") ;;
*) echo "unsafe staging Docker command" >&2; exit 1 ;;
esac
for key in "${required[@]}"; do
if [ -z "${!key:-}" ]; then
echo "required staging deployment input is missing: $key" >&2
@@ -14,7 +19,7 @@ for key in "${required[@]}"; do
done
sha_pattern='^[0-9a-f]{40}$'
digest_pattern='^ghcr\.io/jesse-ux/jyotisha-(api|web)@sha256:[0-9a-f]{64}$'
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}$'
image_id_pattern='^sha256:[0-9a-f]{64}$'
if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] ||
[[ ! "$API_IMAGE" =~ $digest_pattern ]] ||
@@ -22,12 +27,14 @@ 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
fi
case "$INCOMING_PATH" in
"$DEPLOY_PATH"/.incoming/*) ;;
/tmp/jyotisha-staging.*) ;;
*) echo "unsafe incoming staging path" >&2; exit 1 ;;
esac
@@ -43,11 +50,11 @@ current_sha="not-deployed"
if [ -f "$state_directory/deployed-revision" ]; then
current_sha="$(<"$state_directory/deployed-revision")"
else
existing_web="$(docker ps -aq \
existing_web="$("${docker_command[@]}" ps -aq \
--filter 'label=com.docker.compose.project=jyotisha-staging' \
--filter 'label=com.docker.compose.service=web' | head -n 1)"
if [ -n "$existing_web" ]; then
discovered_sha="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \
discovered_sha="$("${docker_command[@]}" inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \
"$existing_web" | sed -n 's/^GITHUB_SHA=//p' | head -n 1)"
if [ -n "$discovered_sha" ]; then current_sha="$discovered_sha"; fi
fi
@@ -69,7 +76,7 @@ if [ "$ALLOW_ROLLBACK" = "false" ] &&
fi
container_id() {
docker ps -aq \
"${docker_command[@]}" ps -aq \
--filter 'label=com.docker.compose.project=jyotisha-staging' \
--filter "label=com.docker.compose.service=$1" | head -n 1
}
@@ -80,20 +87,20 @@ repo_digest_for_container() {
local id image_id
id="$(container_id "$service")"
[ -n "$id" ] || return 0
image_id="$(docker inspect --format '{{.Image}}' "$id")"
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image_id" |
image_id="$("${docker_command[@]}" inspect --format '{{.Image}}' "$id")"
"${docker_command[@]}" image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image_id" |
awk -v prefix="$repository@sha256:" 'index($0, prefix) == 1 { print; exit }'
}
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_image="$(repo_digest_for_container api "$api_repository")"
previous_web_image="$(repo_digest_for_container web "$web_repository")"
previous_api_id=""
previous_web_id=""
if [ -n "$(container_id api)" ]; then
previous_api_id="$(docker inspect --format '{{.Image}}' "$(container_id api)")"
previous_api_id="$("${docker_command[@]}" inspect --format '{{.Image}}' "$(container_id api)")"
fi
if [ -n "$(container_id web)" ]; then
previous_web_id="$(docker inspect --format '{{.Image}}' "$(container_id web)")"
previous_web_id="$("${docker_command[@]}" inspect --format '{{.Image}}' "$(container_id web)")"
fi
rollback_image() {
@@ -118,7 +125,7 @@ bash deploy/validate-staging-env.sh \
bash deploy/validate-staging-database-env.sh .env.staging.database
compose=(
docker compose -p jyotisha-staging --env-file .env.staging
"${docker_command[@]}" compose -p jyotisha-staging --env-file .env.staging
-f deploy/docker-compose.server.yml -f deploy/docker-compose.postgres.yml
-f deploy/docker-compose.staging.yml
)
@@ -126,7 +133,6 @@ 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
@@ -172,10 +178,10 @@ verify_container_image() {
local id expected_id running_id repo_digests
id="$(container_id "$service")"
[ -n "$id" ]
expected_id="$(docker image inspect --format '{{.Id}}' "$expected_ref")"
running_id="$(docker inspect --format '{{.Image}}' "$id")"
expected_id="$("${docker_command[@]}" image inspect --format '{{.Id}}' "$expected_ref")"
running_id="$("${docker_command[@]}" inspect --format '{{.Image}}' "$id")"
[ "$running_id" = "$expected_id" ]
repo_digests="$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$expected_id")"
repo_digests="$("${docker_command[@]}" image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$expected_id")"
grep -Fqx "$expected_ref" <<<"$repo_digests"
}
verify_container_image api "$API_IMAGE"
@@ -184,7 +190,6 @@ 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,15 +201,10 @@ for (let attempt = 0; attempt < 12; attempt += 1) {
await delay(5_000);
}
if (!login?.ok) 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 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 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`);
+12 -6
View File
@@ -6,6 +6,11 @@ required=(
INCOMING_PATH DEPLOY_PATH WEB_IMAGE DEPLOY_SHA EXPECTED_PREVIOUS_SHA
DOCKER_CONFIG
)
case "${DOCKER_BIN:-docker}" in
docker) docker_command=(docker) ;;
"sudo -n docker") docker_command=(sudo -n docker --config "$DOCKER_CONFIG") ;;
*) echo "unsafe staging Docker command" >&2; exit 1 ;;
esac
for key in "${required[@]}"; do
if [ -z "${!key:-}" ]; then
echo "required staging migration input is missing: $key" >&2
@@ -17,12 +22,13 @@ done
echo "unsafe staging migration revision" >&2
exit 1
}
[[ "$WEB_IMAGE" =~ ^ghcr\.io/jesse-ux/jyotisha-web@sha256:[0-9a-f]{64}$ ]] || {
image_pattern='^crpi-d1feco6itet73spp\.cn-hongkong\.personal\.cr\.aliyuncs\.com/copse/jyotisha@sha256:[0-9a-f]{64}$'
[[ "$WEB_IMAGE" =~ $image_pattern ]] || {
echo "unsafe staging migration image" >&2
exit 1
}
case "$INCOMING_PATH" in
"$DEPLOY_PATH"/.incoming/*) ;;
/tmp/jyotisha-staging.*) ;;
*) echo "unsafe incoming staging path" >&2; exit 1 ;;
esac
@@ -38,11 +44,11 @@ current_sha="not-deployed"
if [ -f "$state_directory/deployed-revision" ]; then
current_sha="$(<"$state_directory/deployed-revision")"
else
existing_web="$(docker ps -aq \
existing_web="$("${docker_command[@]}" ps -aq \
--filter 'label=com.docker.compose.project=jyotisha-staging' \
--filter 'label=com.docker.compose.service=web' | head -n 1)"
if [ -n "$existing_web" ]; then
discovered_sha="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \
discovered_sha="$("${docker_command[@]}" inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \
"$existing_web" | sed -n 's/^GITHUB_SHA=//p' | head -n 1)"
if [ -n "$discovered_sha" ]; then current_sha="$discovered_sha"; fi
fi
@@ -71,8 +77,8 @@ bash deploy/validate-staging-env.sh \
bash deploy/validate-staging-database-env.sh .env.staging.database
export DATABASE_ENV_FILE='../.env.staging.database'
compose=(docker compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml)
docker pull "$WEB_IMAGE"
compose=("${docker_command[@]}" compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml)
"${docker_command[@]}" pull "$WEB_IMAGE"
"${compose[@]}" up -d --no-build --pull never --wait postgres
"${compose[@]}" exec -T postgres psql -v ON_ERROR_STOP=1 -U postgres -d jyotisha \
-f /dev/stdin < deploy/postgres/002-ensure-business-compatibility-roles.sql
+7 -9
View File
@@ -41,11 +41,9 @@ 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"
@@ -88,13 +86,6 @@ 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
@@ -106,6 +97,13 @@ 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"