1b8d6fcce6
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.
187 lines
10 KiB
YAML
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'"
|