Merge GitHub upstream into Gitea primary
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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[@]}")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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 ./
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user