name: Staging Backend Quality Gate on: pull_request: paths: - '.gitea/workflows/backend-quality-gate.yml' - '.gitea/workflows/deploy-staging.yml' - '.gitea/workflows/migrate-staging-database.yml' - '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 permissions: contents: read actions: write jobs: validate: runs-on: xiaoxin timeout-minutes: 45 env: GITEA_SHA: ${{ gitea.sha }} steps: - name: Checkout exact Gitea revision run: | set -euo pipefail [[ "$GITEA_SHA" =~ ^[0-9a-f]{40}$ ]] git init . git remote remove origin 2>/dev/null || true git remote add origin https://git.copse.top/root/Jyotisha.git git -c http.connectTimeout=15 -c http.lowSpeedLimit=1024 -c http.lowSpeedTime=30 \ fetch --depth=1 --no-tags origin "$GITEA_SHA" git checkout --detach --force "$GITEA_SHA" git clean -ffdx test "$(git rev-parse HEAD)" = "$GITEA_SHA" test -z "$(git status --porcelain --untracked-files=all)" - name: Verify Linux runner toolchain run: | set -euo pipefail python3 --version node --version npm --version curl --version docker version - name: Install and verify Docker Compose v2 env: DOCKER_COMPOSE_VERSION: v2.39.1 run: | set -euo pipefail case "$(uname -m)" in x86_64) compose_arch=x86_64; compose_sha=a5ea28722d5da628b59226626f7d6c33c89a7ed19e39f750645925242044c9d2 ;; aarch64|arm64) compose_arch=aarch64; compose_sha=7b2627ed76f7dcb0d93f649f185af912372229b4c09762a3cd1db5be5255632b ;; *) echo "Unsupported Docker Compose architecture: $(uname -m)" >&2; exit 1 ;; esac compose_dir="${DOCKER_CONFIG:-$HOME/.docker}/cli-plugins" compose_bin="$compose_dir/docker-compose" mkdir -p "$compose_dir" curl -fsSL \ "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-$compose_arch" \ -o "$compose_bin" printf '%s %s\n' "$compose_sha" "$compose_bin" | sha256sum -c - chmod +x "$compose_bin" docker compose version docker compose version --short | grep -Eq '^v?2\.' docker compose --help | grep -q -- '--project-name' - name: Install dependencies run: | set -euo pipefail python3 -m venv .venv export PATH="$PWD/.venv/bin:$PATH" python -m pip install --upgrade pip python -m pip install -r requirements.txt -r requirements-dev.txt playwright python -m playwright install --with-deps chromium npm ci --prefix frontend - name: Validate backend, package, frontend, and database contracts run: | set -euo pipefail export PATH="$PWD/.venv/bin:$PATH" 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 python -m py_compile scripts/*.py jyotish_vedic/*.py mcp_server.py python scripts/run_quality_gate.py \ --profile quick --skip-yoga-logic --skip-frontend-runtime python scripts/commercial_privacy_artifact_scan.py --json python -m build npm test --prefix frontend npm run lint --prefix frontend npm run build --prefix frontend publish: if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/staging' needs: validate runs-on: xiaoxin timeout-minutes: 45 env: GITEA_SHA: ${{ gitea.sha }} 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 steps: - name: Checkout exact Gitea revision run: | set -euo pipefail [[ "$GITEA_SHA" =~ ^[0-9a-f]{40}$ ]] git init . git remote remove origin 2>/dev/null || true git remote add origin https://git.copse.top/root/Jyotisha.git git -c http.connectTimeout=15 -c http.lowSpeedLimit=1024 -c http.lowSpeedTime=30 \ fetch --depth=1 --no-tags origin "$GITEA_SHA" git checkout --detach --force "$GITEA_SHA" git clean -ffdx test "$(git rev-parse HEAD)" = "$GITEA_SHA" test -z "$(git status --porcelain --untracked-files=all)" - name: Build and publish exact-SHA ACR images env: REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: | set -euo pipefail cleanup() { docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true; } trap cleanup EXIT printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin docker build -f deploy/railway-api.Dockerfile -t "$IMAGE_REPOSITORY:api-$GITEA_SHA" . docker build -f deploy/railway-web.Dockerfile -t "$IMAGE_REPOSITORY:web-$GITEA_SHA" . docker push "$IMAGE_REPOSITORY:api-$GITEA_SHA" docker push "$IMAGE_REPOSITORY:web-$GITEA_SHA" - name: Record immutable linux-amd64 image manifest run: | set -euo pipefail [[ "$GITEA_SHA" =~ ^[0-9a-f]{40}$ ]] [[ "$GITEA_RUN_ATTEMPT" =~ ^[0-9]+$ ]] select_digest='import json,sys; d=json.load(sys.stdin); xs=d if isinstance(d,list) else [d]; xs=[x for x in xs if isinstance(x,dict) and isinstance(x.get("Descriptor",x),dict)]; x=next((x for x in xs if x.get("Descriptor",x).get("platform",{}).get("os")=="linux" and x.get("Descriptor",x).get("platform",{}).get("architecture")=="amd64"),None); print(x.get("Descriptor",x).get("digest","") if x else "")' api_digest="$(docker manifest inspect "$IMAGE_REPOSITORY:api-$GITEA_SHA" --verbose | python3 -c "$select_digest")" web_digest="$(docker manifest inspect "$IMAGE_REPOSITORY:web-$GITEA_SHA" --verbose | python3 -c "$select_digest")" [[ "$api_digest" =~ ^sha256:[0-9a-f]{64}$ ]] [[ "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] install -d -m 700 artifacts/staging-images umask 077 printf 'git_sha=%s\napi_digest=%s\nweb_digest=%s\n' \ "$GITEA_SHA" "$api_digest" "$web_digest" \ > artifacts/staging-images/manifest.env node frontend/scripts/staging-image-manifest.mjs \ artifacts/staging-images/manifest.env "$GITEA_SHA" "$IMAGE_REPOSITORY" >/dev/null - name: Upload immutable staging image manifest uses: https://gitea.com/actions/upload-artifact@v4 with: name: staging-image-manifest-${{ gitea.sha }}-${{ gitea.run_attempt }} path: artifacts/staging-images/manifest.env if-no-files-found: error retention-days: 30