Files
Jyotisha/.gitea/workflows/backend-quality-gate.yml
T
Jesse_ChenandClaude Fable 5.1 f7428909d2
Independent Staging Quality Gate / validate (push) Successful in 21m36s
Independent Staging Quality Gate / publish (push) Successful in 1m56s
ci(gate): drop the removed GitHub workflow directory from the gated path list
The mirror workflows were deleted in the same release, so the gated
path list, both trigger filters and the contract test no longer name
.github/workflows/**.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VawU7Xfd5jS9wUEXz1XYmS
2026-09-02 04:02:04 +00:00

540 lines
24 KiB
YAML

name: Independent Staging Quality Gate
on:
# Both path lists are generated from deploy/gated-paths.txt (single source of
# truth, enforced by frontend/tests/staging-backend-workflows.test.ts). A push
# touching none of them is docs-only: it neither reruns this gate nor cancels
# a gate already running for a code push.
pull_request:
paths:
- '.dockerignore'
- '.gitea/**'
- 'MANIFEST.in'
- 'mcp_server.py'
- 'pyproject.toml'
- 'requirements*.txt'
- 'jyotish_vedic/**'
- 'scripts/**'
- 'tests/**'
- 'SKILL.md'
- 'assets/**'
- 'references/**'
- 'skills/**'
- 'deploy/**'
- 'frontend/**'
- 'contracts/**'
push:
branches: [staging]
paths:
- '.dockerignore'
- '.gitea/**'
- 'MANIFEST.in'
- 'mcp_server.py'
- 'pyproject.toml'
- 'requirements*.txt'
- 'jyotish_vedic/**'
- 'scripts/**'
- 'tests/**'
- 'SKILL.md'
- 'assets/**'
- 'references/**'
- 'skills/**'
- 'deploy/**'
- 'frontend/**'
- 'contracts/**'
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 }}
GITEA_EVENT_NAME: ${{ gitea.event_name }}
NODE_TOOL_SOURCE_IMAGE: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c
NODE_TOOL_IMAGE: node:22-bookworm-slim
steps:
- name: Checkout exact Gitea revision
env:
MIRROR_PATH: /root/.cache/jyotisha-mirror.git
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
bounded_git() {
timeout 300 git -c http.connectTimeout=15 -c http.lowSpeedLimit=1 -c http.lowSpeedTime=60 "$@"
}
fetch_succeeded=false
# act_runner's hostexecutor discards this workspace after every run but
# keeps the host filesystem, so a bare mirror at MIRROR_PATH amortises
# the 105 MB tree across runs; the exact SHA is then fetched from local
# disk in seconds instead of 4-8 minutes per job over the WAN. The
# mirror is an accelerator, never a dependency: every failure below
# falls through to the bounded remote fetch that has always been used.
sync_mirror() {
if [ -d "$MIRROR_PATH" ] && [ "$(git -C "$MIRROR_PATH" rev-parse --is-bare-repository 2>/dev/null)" = true ]; then
# We hold the host lock, so any git lock file left by a cancelled job is stale.
find "$MIRROR_PATH" -name '*.lock' -type f -delete 2>/dev/null || true
if ! git -C "$MIRROR_PATH" cat-file -e "$GITEA_SHA^{commit}" 2>/dev/null; then
bounded_git -C "$MIRROR_PATH" fetch --prune origin || return 1
fi
else
rm -rf "$MIRROR_PATH"
timeout 900 git -c http.connectTimeout=15 -c http.lowSpeedLimit=1 -c http.lowSpeedTime=60 \
clone --quiet --mirror https://git.copse.top/root/Jyotisha.git "$MIRROR_PATH" || { rm -rf "$MIRROR_PATH"; return 1; }
fi
git -C "$MIRROR_PATH" cat-file -e "$GITEA_SHA^{commit}"
}
if mkdir -p "$(dirname "$MIRROR_PATH")" 2>/dev/null && exec 9>"$MIRROR_PATH.lock" 2>/dev/null; then
if flock -w 900 9; then
if sync_mirror; then
git remote set-url origin "$MIRROR_PATH"
if timeout 300 git fetch --no-tags origin "$GITEA_SHA"; then
fetch_succeeded=true
else
echo "mirror fetch of $GITEA_SHA failed; falling back to remote fetch" >&2
fi
git remote set-url origin https://git.copse.top/root/Jyotisha.git
else
echo "mirror sync at $MIRROR_PATH failed; falling back to remote fetch" >&2
fi
flock -u 9
else
echo "mirror lock $MIRROR_PATH.lock is busy; falling back to remote fetch" >&2
fi
exec 9>&-
else
echo "mirror path $MIRROR_PATH is unavailable; falling back to remote fetch" >&2
fi
if [ "$fetch_succeeded" != true ]; then
for attempt in 1 2 3; do
if bounded_git fetch --depth=1 --no-tags origin "$GITEA_SHA"; then
fetch_succeeded=true
break
fi
if [ "$attempt" -eq 3 ]; then
echo "exact staging gate checkout failed after $attempt bounded attempts" >&2
exit 1
fi
sleep $((attempt * 10))
done
fi
[[ "$fetch_succeeded" == true ]]
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: Install and verify Linux runner toolchain
run: |
set -euo pipefail
packages=()
if ! docker compose version --short 2>/dev/null | grep -Eq '^v?2\.'; then
packages+=(docker-compose-v2)
fi
venv_probe="$(mktemp -d "${RUNNER_TEMP:-/tmp}/jyotisha-venv-probe.XXXXXX")"
if ! python3 -m venv "$venv_probe/venv" >/dev/null 2>&1; then
packages+=(python3-venv)
fi
rm -rf "$venv_probe"
if ! python3 -c 'import pathlib, sysconfig; assert pathlib.Path(sysconfig.get_path("include"), "Python.h").is_file()' >/dev/null 2>&1; then
packages+=(python3-dev)
fi
if ! command -v g++ >/dev/null 2>&1; then
packages+=(g++)
fi
if ! command -v rsync >/dev/null 2>&1; then
packages+=(rsync)
fi
if [ "${#packages[@]}" -gt 0 ]; then
apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
"${packages[@]}"
fi
python3 --version
curl --version
git --version
openssl version
docker version
docker compose version
docker compose version --short | grep -Eq '^v?2\.'
docker compose --help | grep -q -- '--project-name'
- name: Reclaim runner disk
env:
KEEP_IMAGE_SHA: ${{ gitea.sha }}
run: bash deploy/reclaim-runner-disk.sh
- name: Prepare pinned Node tooling
run: |
set -euo pipefail
if ! docker image inspect "$NODE_TOOL_SOURCE_IMAGE" >/dev/null 2>&1; then
for attempt in 1 2 3; do
if timeout 180 docker pull "$NODE_TOOL_SOURCE_IMAGE"; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to preload $NODE_TOOL_IMAGE after $attempt attempts" >&2
exit 1
fi
sleep $((attempt * 15))
done
fi
docker tag "$NODE_TOOL_SOURCE_IMAGE" "$NODE_TOOL_IMAGE"
docker image inspect "$NODE_TOOL_IMAGE" >/dev/null
tool_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/jyotisha-node-tools.XXXXXX")"
container_id="$(docker create "$NODE_TOOL_IMAGE")"
trap 'docker rm -f "$container_id" >/dev/null 2>&1 || true' EXIT
docker cp "$container_id:/usr/local/bin/node" "$tool_dir/node"
docker cp "$container_id:/usr/local/lib/node_modules/npm" "$tool_dir/npm-package"
docker rm "$container_id" >/dev/null
trap - EXIT
ln -s "$tool_dir/npm-package/bin/npm-cli.js" "$tool_dir/npm"
chmod 0755 "$tool_dir/node" "$tool_dir/npm-package/bin/npm-cli.js"
test -n "${GITHUB_PATH:-}"
printf '%s\n' "$tool_dir" >> "$GITHUB_PATH"
export PATH="$tool_dir:$PATH"
node --version | grep -Eq '^v22\.'
npm --version
- name: Preload PostgreSQL integration image
env:
POSTGRES_TEST_SOURCE_IMAGE: public.ecr.aws/docker/library/postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193
POSTGRES_TEST_IMAGE: postgres:17-alpine
run: |
set -euo pipefail
if ! docker image inspect "$POSTGRES_TEST_IMAGE" >/dev/null 2>&1; then
for attempt in 1 2 3; do
if timeout 180 docker pull "$POSTGRES_TEST_SOURCE_IMAGE"; then
docker tag "$POSTGRES_TEST_SOURCE_IMAGE" "$POSTGRES_TEST_IMAGE"
break
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to preload $POSTGRES_TEST_IMAGE after $attempt attempts" >&2
exit 1
fi
sleep $((attempt * 15))
done
fi
docker image inspect "$POSTGRES_TEST_IMAGE" >/dev/null
- name: Install dependencies
env:
PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple/
NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
run: |
set -euo pipefail
python3 -m venv .venv
export PATH="$PWD/.venv/bin:$PATH"
python -m pip install --upgrade pip
python -m pip install \
"mcp==1.28.1" \
"pydantic==2.13.4" \
"numpy==2.5.1" \
"pandas==2.3.3" \
"timezonefinder==8.2.5" \
-r requirements.txt -r requirements-dev.txt
workdir="$(pwd -P)"
set +e
docker run --rm \
--cpus=1.5 \
--memory=2g \
--memory-swap=2g \
--pids-limit=256 \
--user "$(id -u):$(id -g)" \
--volume "$workdir:$workdir" \
--workdir "$workdir" \
--env HOME=/tmp \
--env "NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY" \
"$NODE_TOOL_SOURCE_IMAGE" \
timeout --signal=TERM --kill-after=30s 900s \
npm ci --prefix frontend \
--no-audit \
--no-fund \
--progress=false \
--maxsockets=4 \
--fetch-timeout=60000 \
--fetch-retries=2 \
--fetch-retry-mintimeout=1000 \
--fetch-retry-maxtimeout=10000
npm_ci_status=$?
set -e
if [ "$npm_ci_status" -eq 124 ]; then
echo "frontend npm ci exceeded bounded 900-second timeout; check npm mirror/network or dependency postinstall hang" >&2
exit 124
fi
if [ "$npm_ci_status" -ne 0 ]; then
echo "frontend npm ci failed with status $npm_ci_status inside bounded Node container" >&2
exit "$npm_ci_status"
fi
- 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
if [ "$GITEA_EVENT_NAME" != "push" ]; then
if ! timeout 600 npm run build --prefix frontend -- --webpack; then
echo "frontend production build exceeded bounded 600-second timeout" >&2
exit 124
fi
else
echo "staging push production build is verified once by the publish image build"
fi
publish:
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/staging'
needs: validate
runs-on: xiaoxin
timeout-minutes: 60
env:
GITEA_SHA: ${{ gitea.sha }}
GITEA_RUN_ATTEMPT: ${{ gitea.run_attempt }}
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
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
env:
MIRROR_PATH: /root/.cache/jyotisha-mirror.git
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
bounded_git() {
timeout 300 git -c http.connectTimeout=15 -c http.lowSpeedLimit=1 -c http.lowSpeedTime=60 "$@"
}
fetch_succeeded=false
# act_runner's hostexecutor discards this workspace after every run but
# keeps the host filesystem, so a bare mirror at MIRROR_PATH amortises
# the 105 MB tree across runs; the exact SHA is then fetched from local
# disk in seconds instead of 4-8 minutes per job over the WAN. The
# mirror is an accelerator, never a dependency: every failure below
# falls through to the bounded remote fetch that has always been used.
sync_mirror() {
if [ -d "$MIRROR_PATH" ] && [ "$(git -C "$MIRROR_PATH" rev-parse --is-bare-repository 2>/dev/null)" = true ]; then
# We hold the host lock, so any git lock file left by a cancelled job is stale.
find "$MIRROR_PATH" -name '*.lock' -type f -delete 2>/dev/null || true
if ! git -C "$MIRROR_PATH" cat-file -e "$GITEA_SHA^{commit}" 2>/dev/null; then
bounded_git -C "$MIRROR_PATH" fetch --prune origin || return 1
fi
else
rm -rf "$MIRROR_PATH"
timeout 900 git -c http.connectTimeout=15 -c http.lowSpeedLimit=1 -c http.lowSpeedTime=60 \
clone --quiet --mirror https://git.copse.top/root/Jyotisha.git "$MIRROR_PATH" || { rm -rf "$MIRROR_PATH"; return 1; }
fi
git -C "$MIRROR_PATH" cat-file -e "$GITEA_SHA^{commit}"
}
if mkdir -p "$(dirname "$MIRROR_PATH")" 2>/dev/null && exec 9>"$MIRROR_PATH.lock" 2>/dev/null; then
if flock -w 900 9; then
if sync_mirror; then
git remote set-url origin "$MIRROR_PATH"
if timeout 300 git fetch --no-tags origin "$GITEA_SHA"; then
fetch_succeeded=true
else
echo "mirror fetch of $GITEA_SHA failed; falling back to remote fetch" >&2
fi
git remote set-url origin https://git.copse.top/root/Jyotisha.git
else
echo "mirror sync at $MIRROR_PATH failed; falling back to remote fetch" >&2
fi
flock -u 9
else
echo "mirror lock $MIRROR_PATH.lock is busy; falling back to remote fetch" >&2
fi
exec 9>&-
else
echo "mirror path $MIRROR_PATH is unavailable; falling back to remote fetch" >&2
fi
if [ "$fetch_succeeded" != true ]; then
for attempt in 1 2 3; do
if bounded_git fetch --depth=1 --no-tags origin "$GITEA_SHA"; then
fetch_succeeded=true
break
fi
if [ "$attempt" -eq 3 ]; then
echo "exact staging gate checkout failed after $attempt bounded attempts" >&2
exit 1
fi
sleep $((attempt * 10))
done
fi
[[ "$fetch_succeeded" == true ]]
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: Reclaim runner disk
env:
KEEP_IMAGE_SHA: ${{ gitea.sha }}
run: bash deploy/reclaim-runner-disk.sh
- name: Prepare pinned Node tooling
env:
NODE_TOOL_SOURCE_IMAGE: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c
NODE_TOOL_IMAGE: node:22-bookworm-slim
run: |
set -euo pipefail
if ! docker image inspect "$NODE_TOOL_SOURCE_IMAGE" >/dev/null 2>&1; then
for attempt in 1 2 3; do
if timeout 180 docker pull "$NODE_TOOL_SOURCE_IMAGE"; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to preload $NODE_TOOL_IMAGE after $attempt attempts" >&2
exit 1
fi
sleep $((attempt * 15))
done
fi
docker tag "$NODE_TOOL_SOURCE_IMAGE" "$NODE_TOOL_IMAGE"
docker image inspect "$NODE_TOOL_IMAGE" >/dev/null
tool_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/jyotisha-node-tools.XXXXXX")"
cat > "$tool_dir/node" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
workdir="$(pwd -P)"
exec docker run --rm \
--user "$(id -u):$(id -g)" \
--volume "$workdir:$workdir" \
--workdir "$workdir" \
--env HOME=/tmp \
node:22-bookworm-slim "${0##*/}" "$@"
EOF
chmod 0755 "$tool_dir/node"
ln -s node "$tool_dir/npm"
test -n "${GITHUB_PATH:-}"
printf '%s\n' "$tool_dir" >> "$GITHUB_PATH"
export PATH="$tool_dir:$PATH"
node --version
npm --version
- name: Build and publish exact-SHA ACR images
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
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 --build-arg NEXT_DEPLOYMENT_ID="$GITEA_SHA" -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
git archive --format=tar --output artifacts/staging-images/controller.tar \
"$GITEA_SHA" deploy frontend/scripts/staging-image-manifest.mjs
controller_sha256="$(sha256sum artifacts/staging-images/controller.tar | awk '{print $1}')"
[[ "$controller_sha256" =~ ^[0-9a-f]{64}$ ]]
printf 'git_sha=%s\napi_digest=%s\nweb_digest=%s\ncontroller_sha256=%s\n' \
"$GITEA_SHA" "$api_digest" "$web_digest" "$controller_sha256" \
> 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
run: |
set -euo pipefail
test -n "${ACTIONS_RUNTIME_TOKEN:-}"
test -n "${ACTIONS_RESULTS_URL:-}"
test -n "${GITHUB_RUN_ID:-}"
test -n "${GITHUB_REPOSITORY:-}"
workdir="$(pwd -P)"
docker run --rm \
--user "$(id -u):$(id -g)" \
--volume "$workdir:$workdir" \
--workdir "$workdir" \
--env HOME=/tmp \
--env "INPUT_NAME=staging-image-manifest-$GITEA_SHA-$GITEA_RUN_ATTEMPT" \
--env INPUT_PATH=artifacts/staging-images/ \
--env INPUT_OVERWRITE=false \
--env ACTIONS_RUNTIME_TOKEN \
--env ACTIONS_RESULTS_URL \
--env GITHUB_RUN_ID \
--env GITHUB_REPOSITORY \
--env "GITHUB_SHA=$GITEA_SHA" \
--env "GITHUB_WORKSPACE=$workdir" \
node:22-bookworm-slim \
node -e 'process.env["INPUT_IF-NO-FILES-FOUND"]="error"; process.env["INPUT_RETENTION-DAYS"]="30"; process.env["INPUT_COMPRESSION-LEVEL"]="6"; require("./.gitea/actions/upload-artifact/dist/index.js")'
- name: Dispatch exact-SHA staging deployment
env:
DEPLOY_SHA: ${{ gitea.sha }}
run: |
set -euo pipefail
[[ "$DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]]
gate_run_id="${GITHUB_RUN_ID:-}"
[[ "$gate_run_id" =~ ^[0-9]+$ ]]
current_staging_sha="$(curl --fail --silent --show-error --connect-timeout 15 --max-time 60 --retry 3 --retry-all-errors \
--header "Authorization: token $GITEA_TOKEN" \
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/git/refs/heads/staging" |
jq -er 'select(type == "array" and length == 1) | .[0] |
select(.ref == "refs/heads/staging") | .object.sha |
select(test("^[0-9a-f]{40}$"))')"
if [[ "$current_staging_sha" != "$DEPLOY_SHA" ]]; then
# Docs-only pushes (every change outside deploy/gated-paths.txt) no
# longer run this gate, so staging may legitimately sit ahead of the
# tested SHA. Release only when the whole range is docs-only; a
# diverged, older, or code-bearing head is still refused. The Gitea
# compare API is used because this checkout is shallow and the
# newer head is not in local history.
if bash deploy/is-docs-only-range.sh --api "$DEPLOY_SHA" "$current_staging_sha"; then
echo "staging advanced to $current_staging_sha by docs-only commits; releasing tested $DEPLOY_SHA"
else
echo "staging advanced before deployment dispatch; refusing stale release" >&2
exit 1
fi
fi
payload="$(jq -cn --arg ref "refs/heads/staging" --arg deploy_sha "$DEPLOY_SHA" --arg gate_run_id "$gate_run_id" \
'{ref:$ref,inputs:{deploy_sha:$deploy_sha,gate_run_id:$gate_run_id,allow_rollback:"false"}}')"
response_file="$(mktemp "${RUNNER_TEMP:-/tmp}/jyotisha-deploy-dispatch.XXXXXX")"
trap 'rm -f -- "$response_file"' EXIT
curl --fail --silent --show-error --request POST \
--header "Authorization: token $GITEA_TOKEN" \
--header "Content-Type: application/json" \
--data "$payload" \
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/actions/workflows/deploy-staging.yml/dispatches?return_run_details=true" \
--output "$response_file"
deploy_run_id="$(jq -er '.workflow_run_id | select(type == "number" and . > 0)' "$response_file")"
echo "Dispatched Deploy staging run $deploy_run_id for $DEPLOY_SHA"
- name: Logout ACR registry
if: always()
run: docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true