Files
Jyotisha/.gitea/workflows/backend-quality-gate.yml
T
linmeng 1b8d6fcce6
Staging Backend Quality Gate (push the reviewed main SHA to staging to auto-deploy) / validate (push) Failing after 5m10s
Staging Backend Quality Gate (push the reviewed main SHA to staging to auto-deploy) / publish-and-deploy (push) Has been skipped
feat: add configurable payments and Gitea staging delivery
Add database-backed administrators, configurable Alipay packages, idempotent payment settlement and platform reporting. Standardize Gitea workflows on the xiaoxin runner so reviewed staging commits are tested, packaged, and deployed by immutable image digest.
2026-07-27 20:19:05 +08:00

187 lines
10 KiB
YAML

name: Staging Backend Quality Gate (push the reviewed main SHA to staging to auto-deploy)
on:
pull_request:
paths:
- '.gitea/workflows/**'
- 'deploy/**'
- 'frontend/**'
- 'jyotish_vedic/**'
- 'scripts/**'
- 'tests/**'
- 'mcp_server.py'
- 'pyproject.toml'
- 'requirements*.txt'
push:
branches: [staging]
workflow_dispatch:
concurrency:
group: staging-quality-${{ gitea.ref }}
cancel-in-progress: true
jobs:
validate:
runs-on: xiaoxin
timeout-minutes: 30
env:
GITEA_SHA: ${{ gitea.sha }}
steps:
- name: Checkout current Gitea revision
run: |
set -euo pipefail
git init .
git remote remove origin 2>/dev/null || true
git remote add origin "https://git.copse.top/root/Jyotisha.git"
git fetch --no-tags origin "${GITEA_SHA}"
git checkout --detach --force "${GITEA_SHA}"
- name: Verify Linux runner toolchain
run: |
set -euo pipefail
python3 --version
node --version
npm --version
docker version
- name: Install dependencies
run: |
set -euo pipefail
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt -r requirements-dev.txt
npm ci --prefix frontend
- name: Validate backend, package, frontend, and database contracts
env:
NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder
run: |
set -euo pipefail
ruff check scripts/run_quality_gate.py tests/test_varga_bphs.py tests/test_ashtakavarga_invariants.py tests/test_cli_smoke.py tests/test_yoga_rules_integrity.py
python3 -m py_compile scripts/*.py jyotish_vedic/*.py mcp_server.py
python3 scripts/run_quality_gate.py --profile quick --skip-yoga-logic --skip-frontend-runtime
python3 scripts/commercial_privacy_artifact_scan.py --json
python3 -m build
npm test --prefix frontend
npm run lint --prefix frontend
npm run build --prefix frontend
publish-and-deploy:
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/staging'
needs: validate
runs-on: xiaoxin
timeout-minutes: 45
env:
GITEA_SHA: ${{ gitea.sha }}
GITEA_REF: ${{ gitea.ref }}
GITEA_EVENT_NAME: ${{ gitea.event_name }}
GITEA_RUN_NUMBER: ${{ gitea.run_number }}
GITEA_RUN_ATTEMPT: ${{ gitea.run_attempt }}
REGISTRY_HOST: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com
IMAGE_REPOSITORY: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha
DEPLOY_HOST: ${{ vars.STAGING_HOST }}
DEPLOY_PORT: ${{ vars.STAGING_PORT }}
DEPLOY_USER: ${{ vars.STAGING_USER }}
DEPLOY_PATH: ${{ vars.STAGING_PATH }}
STAGING_URL: ${{ vars.STAGING_URL }}
STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }}
steps:
- name: Checkout current Gitea revision
run: |
set -euo pipefail
git init .
git remote remove origin 2>/dev/null || true
git remote add origin "https://git.copse.top/root/Jyotisha.git"
git fetch --no-tags origin main "${GITEA_SHA}"
git checkout --detach --force "${GITEA_SHA}"
- name: Build, publish, and deploy immutable staging images
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
run: |
set -euo pipefail
[[ "${GITEA_EVENT_NAME:-}" == "push" && "${GITEA_REF:-}" == "refs/heads/staging" ]] || {
echo "not an exact staging push" >&2
exit 1
}
[[ "${GITEA_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || { echo "invalid staging commit SHA" >&2; exit 1; }
[[ "${DEPLOY_HOST:-}" =~ ^[A-Za-z0-9.-]+$ ]] || { echo "invalid staging host" >&2; exit 1; }
[[ "${DEPLOY_PORT:-}" =~ ^[1-9][0-9]{0,4}$ ]] || { echo "invalid staging port" >&2; exit 1; }
[[ "${DEPLOY_USER:-}" =~ ^[a-z_][a-z0-9_-]*$ ]] || { echo "invalid staging user" >&2; exit 1; }
[[ "${DEPLOY_PATH:-}" =~ ^/[A-Za-z0-9._/-]+$ ]] || { echo "invalid staging path" >&2; exit 1; }
[[ "${STAGING_URL:-}" =~ ^https://[A-Za-z0-9.-]+(:[1-9][0-9]{0,4})?$ ]] || { echo "invalid staging URL" >&2; exit 1; }
[[ -n "${STAGING_KNOWN_HOSTS:-}" && -n "${REGISTRY_USERNAME:-}" && -n "${REGISTRY_PASSWORD:-}" && -n "${SSH_PRIVATE_KEY:-}" ]] || {
echo "required staging credentials or configuration are missing" >&2
exit 1
}
remote_sha="$(git ls-remote origin refs/heads/staging | awk '{print $1}')"
[[ "$remote_sha" == "$GITEA_SHA" ]] || { echo "staging head changed before publication" >&2; exit 1; }
git fetch origin main
git merge-base --is-ancestor "$GITEA_SHA" origin/main || {
echo "staging revision is not in reviewed main history" >&2
exit 1
}
ssh_root="${RUNNER_TEMP}/jyotisha-staging-ssh"
key_path="${ssh_root}/id_ed25519"
known_hosts_path="${ssh_root}/known_hosts"
archive_path="${RUNNER_TEMP}/deploy-${GITEA_RUN_NUMBER}-${GITEA_RUN_ATTEMPT}.tar"
incoming="${DEPLOY_PATH}/.incoming/${GITEA_RUN_NUMBER}-${GITEA_RUN_ATTEMPT}"
remote_prepared=false
mkdir -p "$ssh_root"
umask 077
printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_path"
printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$known_hosts_path"
chmod 600 "$key_path" "$known_hosts_path"
ssh_options=(-i "$key_path" -p "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path")
scp_options=(-i "$key_path" -P "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path")
remote="${DEPLOY_USER}@${DEPLOY_HOST}"
cleanup() {
if [[ "$remote_prepared" == true ]]; then
ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker logout '$REGISTRY_HOST' >/dev/null 2>&1 || true; rm -rf -- '$incoming'" >/dev/null 2>&1 || true
fi
docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
rm -rf -- "$ssh_root" "$archive_path"
}
trap cleanup EXIT
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin
api_tag="${IMAGE_REPOSITORY}:api-${GITEA_SHA}"
web_tag="${IMAGE_REPOSITORY}:web-${GITEA_SHA}"
docker build --file deploy/railway-api.Dockerfile --tag "$api_tag" .
docker push "$api_tag"
docker build --file deploy/railway-web.Dockerfile --tag "$web_tag" .
docker push "$web_tag"
api_ref="$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$api_tag" | grep -E "^${IMAGE_REPOSITORY}@sha256:[0-9a-f]{64}$" | head -n 1)"
web_ref="$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$web_tag" | grep -E "^${IMAGE_REPOSITORY}@sha256:[0-9a-f]{64}$" | head -n 1)"
[[ -n "$api_ref" && -n "$web_ref" ]] || { echo "immutable image digest was not published" >&2; exit 1; }
manifest_path="${RUNNER_TEMP}/staging-image-manifest.env"
printf 'git_sha=%s\napi_digest=%s\nweb_digest=%s\n' "$GITEA_SHA" "${api_ref#*@}" "${web_ref#*@}" > "$manifest_path"
manifest_output="$(node frontend/scripts/staging-image-manifest.mjs "$manifest_path" "$GITEA_SHA" "$IMAGE_REPOSITORY")"
api_image="$(printf '%s\n' "$manifest_output" | sed -n 's/^api_image=//p')"
web_image="$(printf '%s\n' "$manifest_output" | sed -n 's/^web_image=//p')"
[[ -n "$api_image" && -n "$web_image" ]] || { echo "image manifest output is incomplete" >&2; exit 1; }
tar -cf "$archive_path" deploy
ssh "${ssh_options[@]}" "$remote" "install -d -m 700 '$incoming/.docker'"
remote_prepared=true
scp "${scp_options[@]}" "$archive_path" "${remote}:${incoming}/deploy.tar"
ssh "${ssh_options[@]}" "$remote" "tar -xf '$incoming/deploy.tar' -C '$incoming' && rm -f -- '$incoming/deploy.tar'"
previous_sha="$(ssh "${ssh_options[@]}" "$remote" "state='$DEPLOY_PATH/.state/deployed-revision'; if [ -f \"\$state\" ]; then cat \"\$state\"; else id=\$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1); if [ -n \"\$id\" ]; then value=\$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \"\$id\" | sed -n 's/^GITHUB_SHA=//p' | head -n 1); printf '%s' \"\${value:-not-deployed}\"; else printf not-deployed; fi; fi")"
[[ "$previous_sha" == "not-deployed" || "$previous_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "invalid deployed staging revision state" >&2; exit 1; }
forward_verified=false
if [[ "$previous_sha" != "not-deployed" && "$previous_sha" != "$GITEA_SHA" ]]; then
git cat-file -e "${previous_sha}^{commit}" 2>/dev/null || git fetch origin "$previous_sha"
git merge-base --is-ancestor "$previous_sha" "$GITEA_SHA" || {
echo "automatic rollback or divergent staging deployment refused" >&2
exit 1
}
forward_verified=true
fi
printf '%s' "$REGISTRY_PASSWORD" | ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker login '$REGISTRY_HOST' --username '$REGISTRY_USERNAME' --password-stdin"
ssh "${ssh_options[@]}" "$remote" "INCOMING_PATH='$incoming' DEPLOY_PATH='$DEPLOY_PATH' API_IMAGE='$api_image' WEB_IMAGE='$web_image' DEPLOY_SHA='$GITEA_SHA' EXPECTED_PREVIOUS_SHA='$previous_sha' ALLOW_ROLLBACK='false' FORWARD_REVISION_VERIFIED='$forward_verified' DOCKER_CONFIG='$incoming/.docker' STAGING_URL='$STAGING_URL' bash '$incoming/deploy/run-staging-deploy.sh'"