Files
Jyotisha/.gitea/workflows/backend-quality-gate.yml
T
jesse-ux d021bca5e1
Independent Staging Quality Gate / validate (push) Successful in 12m2s
Independent Staging Quality Gate / publish (push) Successful in 2m8s
fix(ci): clear leftover .venv before Python 3.14 venv create (BUG-677)
Staging gate run 2594 died in Install dependencies with FileExistsError on a reused hostexecutor .venv. Remove the leftover directory and create the venv with --clear so Python 3.14 mkdir does not abort the job.
2026-09-14 09:49:25 +08:00

543 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
# Hostexecutor may keep a gitignored .venv/; Python 3.14 mkdir then
# raises FileExistsError. Clear first instead of reusing leftovers.
rm -rf -- .venv
python3 -m venv --clear .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