ci: remove GitHub mirror workflows and dead Gitea manual workflows
Gitea (git.copse.top) is the only CI/CD control plane; GitHub is a read-only mirror whose Actions are being disabled in repository settings. - Delete all 11 `.github/workflows/*.yml` (stale copies of the old design). - Delete unused Gitea manual workflows `ci.yml`, `test.yml`, `publish-pypi.yml`, `apply-supabase-profile-migrations.yml` (0-1 historical runs, no remaining target). - Fold the full `python -m pytest` tree and `tests/run_all.py` into `release-quality-gate.yml`, which previously only ran the curated release profile; update the `run_quality_gate.py` comment accordingly. - Port `reset-staging-account.yml` to Gitea: `runs-on: xiaoxin`, bounded exact-SHA checkout, `refs/heads/staging` only, `staging-mutation` concurrency, same email/confirmation/host/port/user/path assertions. - Repoint frontend workflow tests at `.gitea/workflows/`, drop the GitHub-only assertions, add coverage for the new reset workflow, and remove `tests/test_supabase_profile_migration_workflow.py`. - Update AGENTS.md §6.8, README.md, and deploy/README.md to the current production/mirror facts and document the staging account reset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VawU7Xfd5jS9wUEXz1XYmS
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1a3e14e726
commit
fb69e43c90
@@ -1,176 +0,0 @@
|
||||
name: Apply production rectification migrations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
operation:
|
||||
description: Check pending migrations or apply them
|
||||
required: true
|
||||
default: check
|
||||
type: choice
|
||||
options:
|
||||
- check
|
||||
- apply
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: production-database-migrations
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
DEPLOY_HOST: 103.117.123.53
|
||||
DEPLOY_PORT: "22000"
|
||||
DEPLOY_USER: root
|
||||
DEPLOY_PATH: /opt/jyotisha-app
|
||||
|
||||
jobs:
|
||||
migrate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout current main revision
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
- name: Reject stale revision
|
||||
run: |
|
||||
tested_sha="$(git rev-parse HEAD)"
|
||||
main_sha="$(git ls-remote origin refs/heads/main | awk '{print $1}')"
|
||||
test "$tested_sha" = "$main_sha" || {
|
||||
echo "Refusing stale migration revision $tested_sha; current main is $main_sha" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Using current main revision $tested_sha"
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.PRODUCTION_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/jyotisha-production
|
||||
chmod 600 ~/.ssh/jyotisha-production
|
||||
printf '%s\n' '[103.117.123.53]:22000 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHQJvN2Mo3Yq8e6ZIK4P2blJ5Vjj0HbknEuk7TyjhMbO' > ~/.ssh/known_hosts
|
||||
|
||||
- name: Upload reviewed migration files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes"
|
||||
SCP_OPTIONS="-i $HOME/.ssh/jyotisha-production -P $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes"
|
||||
REMOTE_DIR="$DEPLOY_PATH/tmp/production-migrations/$GITHUB_RUN_ID"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -m 700 -d '$REMOTE_DIR'"
|
||||
scp $SCP_OPTIONS \
|
||||
frontend/supabase/migrations/20260723030000_align_conversational_follow_up_request.sql \
|
||||
frontend/supabase/migrations/20260724010000_global_birth_locations.sql \
|
||||
frontend/supabase/migrations/20260724020000_align_global_birthplace_rectification_contract.sql \
|
||||
frontend/supabase/migrations/20260724030000_allow_assistant_only_rectification_regenerate.sql \
|
||||
frontend/supabase/migrations/20260725010000_structured_conversational_date_confirmation.sql \
|
||||
frontend/supabase/migrations/20260725020000_repair_structured_conversational_date_validator.sql \
|
||||
frontend/supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_DIR/"
|
||||
|
||||
- name: Check or apply reviewed migrations
|
||||
env:
|
||||
OPERATION: ${{ inputs.operation }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
set +x
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=20"
|
||||
REMOTE_DIR="$DEPLOY_PATH/tmp/production-migrations/$GITHUB_RUN_ID"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"cd '$DEPLOY_PATH' && OPERATION='$OPERATION' REMOTE_DIR='$REMOTE_DIR' bash -s" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
set +x
|
||||
trap 'rm -rf "$REMOTE_DIR"' EXIT
|
||||
case "$OPERATION" in
|
||||
check|apply) ;;
|
||||
*) echo "invalid migration operation" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
ENV_FILE="$PWD/.env.production"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo ".env.production missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
DB_URL="${SUPABASE_DB_URL:-${DATABASE_URL:-}}"
|
||||
if [ -z "$DB_URL" ]; then
|
||||
echo "production database URL is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
psql_query() {
|
||||
docker run --rm postgres:16-alpine \
|
||||
psql "$DB_URL" --set ON_ERROR_STOP=1 --tuples-only --no-align --quiet --command "$1"
|
||||
}
|
||||
|
||||
ledger="$(psql_query "select to_regclass('migration.schema_migrations')")"
|
||||
if [ "$ledger" != "migration.schema_migrations" ]; then
|
||||
echo "production migration ledger is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pending=0
|
||||
for sql_file in \
|
||||
"$REMOTE_DIR/20260723030000_align_conversational_follow_up_request.sql" \
|
||||
"$REMOTE_DIR/20260724010000_global_birth_locations.sql" \
|
||||
"$REMOTE_DIR/20260724020000_align_global_birthplace_rectification_contract.sql" \
|
||||
"$REMOTE_DIR/20260724030000_allow_assistant_only_rectification_regenerate.sql" \
|
||||
"$REMOTE_DIR/20260725010000_structured_conversational_date_confirmation.sql" \
|
||||
"$REMOTE_DIR/20260725020000_repair_structured_conversational_date_validator.sql" \
|
||||
"$REMOTE_DIR/20260726010000_backfill_reported_birth_time_status.sql"
|
||||
do
|
||||
filename="$(basename "$sql_file")"
|
||||
checksum="$(sha256sum "$sql_file" | awk '{print $1}')"
|
||||
recorded="$(psql_query "select checksum from migration.schema_migrations where filename = '$filename'")"
|
||||
if [ -n "$recorded" ]; then
|
||||
test "$recorded" = "$checksum" || {
|
||||
echo "migration checksum mismatch: $filename" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "already applied $filename"
|
||||
continue
|
||||
fi
|
||||
|
||||
pending=$((pending + 1))
|
||||
if [ "$OPERATION" = "check" ]; then
|
||||
echo "pending $filename"
|
||||
continue
|
||||
fi
|
||||
|
||||
wrapped="$REMOTE_DIR/.wrapped-$filename"
|
||||
python3 - "$sql_file" "$wrapped" "$filename" "$checksum" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source_path, target_path, filename, checksum = sys.argv[1:]
|
||||
source = Path(source_path).read_text(encoding="utf-8")
|
||||
source = re.sub(r"\A\s*begin\s*;\s*", "", source, count=1, flags=re.I)
|
||||
source = re.sub(r"\s*commit\s*;\s*\Z", "\n", source, count=1, flags=re.I)
|
||||
ledger = (
|
||||
"\ninsert into migration.schema_migrations (filename, checksum) "
|
||||
f"values ('{filename}', '{checksum}');\n"
|
||||
)
|
||||
Path(target_path).write_text(source + ledger, encoding="utf-8")
|
||||
PY
|
||||
docker run --rm -i postgres:16-alpine \
|
||||
psql "$DB_URL" --set ON_ERROR_STOP=1 --single-transaction --quiet < "$wrapped"
|
||||
verified="$(psql_query "select checksum from migration.schema_migrations where filename = '$filename'")"
|
||||
test "$verified" = "$checksum" || {
|
||||
echo "migration ledger verification failed: $filename" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "applied $filename"
|
||||
done
|
||||
|
||||
if [ "$OPERATION" = "check" ] && [ "$pending" -gt 0 ]; then
|
||||
echo "$pending reviewed production migrations are pending"
|
||||
else
|
||||
echo "production migration state is current"
|
||||
fi
|
||||
REMOTE
|
||||
@@ -1,87 +0,0 @@
|
||||
name: Apply Supabase profile migrations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: supabase-profile-migrations
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
DEPLOY_HOST: 103.117.123.53
|
||||
DEPLOY_PORT: "22000"
|
||||
DEPLOY_USER: root
|
||||
DEPLOY_PATH: /opt/jyotisha-app
|
||||
|
||||
jobs:
|
||||
apply:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout migration files
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.PRODUCTION_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/jyotisha-production
|
||||
chmod 600 ~/.ssh/jyotisha-production
|
||||
printf '%s\n' '[103.117.123.53]:22000 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHQJvN2Mo3Yq8e6ZIK4P2blJ5Vjj0HbknEuk7TyjhMbO' > ~/.ssh/known_hosts
|
||||
|
||||
- name: Copy profile migrations to VPS
|
||||
run: |
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes"
|
||||
RSYNC_SSH="ssh $SSH_OPTIONS"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -m 700 -d '$DEPLOY_PATH/tmp/profile-migrations'"
|
||||
rsync -az -e "$RSYNC_SSH" \
|
||||
frontend/supabase/migrations/20260718050000_profiles_service_role_upsert_grants.sql \
|
||||
frontend/supabase/migrations/20260718060000_profiles_service_role_least_privilege.sql \
|
||||
frontend/supabase/migrations/20260718070000_profiles_service_role_upsert_id.sql \
|
||||
frontend/supabase/migrations/20260718080000_profiles_service_role_account_upsert_selects.sql \
|
||||
frontend/supabase/migrations/20260718100000_repair_missing_chart_profiles.sql \
|
||||
frontend/supabase/migrations/20260718102000_recover_missing_profile_rows.sql \
|
||||
frontend/supabase/migrations/20260718103000_profile_birth_time_declaration_grants.sql \
|
||||
frontend/supabase/migrations/20260718104000_chart_profiles_upsert_id_grant.sql \
|
||||
frontend/supabase/migrations/20260721100000_chat_sessions_delete_grant.sql \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/tmp/profile-migrations/"
|
||||
|
||||
- name: Apply profile migrations using VPS database URL
|
||||
run: |
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "cd '$DEPLOY_PATH' && bash -s" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
set +x
|
||||
ENV_FILE="$PWD/.env.production"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo ".env.production missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
DB_URL="${SUPABASE_DB_URL:-${DATABASE_URL:-}}"
|
||||
if [ -z "$DB_URL" ]; then
|
||||
echo "SUPABASE_DB_URL or DATABASE_URL is required in .env.production" >&2
|
||||
exit 1
|
||||
fi
|
||||
for SQL_FILE in \
|
||||
tmp/profile-migrations/20260718050000_profiles_service_role_upsert_grants.sql \
|
||||
tmp/profile-migrations/20260718060000_profiles_service_role_least_privilege.sql \
|
||||
tmp/profile-migrations/20260718070000_profiles_service_role_upsert_id.sql \
|
||||
tmp/profile-migrations/20260718080000_profiles_service_role_account_upsert_selects.sql \
|
||||
tmp/profile-migrations/20260718100000_repair_missing_chart_profiles.sql \
|
||||
tmp/profile-migrations/20260718102000_recover_missing_profile_rows.sql \
|
||||
tmp/profile-migrations/20260718103000_profile_birth_time_declaration_grants.sql \
|
||||
tmp/profile-migrations/20260718104000_chart_profiles_upsert_id_grant.sql \
|
||||
tmp/profile-migrations/20260721100000_chat_sessions_delete_grant.sql
|
||||
do
|
||||
echo "applying $(basename "$SQL_FILE")"
|
||||
cat "$SQL_FILE" | docker run --rm -i postgres:16-alpine \
|
||||
psql "$DB_URL" --set ON_ERROR_STOP=1 --quiet
|
||||
done
|
||||
REMOTE
|
||||
@@ -1,145 +0,0 @@
|
||||
name: Staging Backend Quality Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/backend-quality-gate.yml'
|
||||
- '.github/workflows/deploy-staging.yml'
|
||||
- '.github/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: backend-quality-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
python -m pip install playwright
|
||||
python -m playwright install --with-deps chrome
|
||||
npm ci --prefix frontend
|
||||
|
||||
- name: Run Python quick quality gate
|
||||
shell: bash
|
||||
run: |
|
||||
set -o 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
|
||||
python -m py_compile scripts/*.py jyotish_vedic/*.py mcp_server.py
|
||||
mkdir -p artifacts
|
||||
python scripts/run_quality_gate.py \
|
||||
--profile quick --skip-yoga-logic --skip-frontend-runtime \
|
||||
2>&1 | tee artifacts/quick-quality-gate.log
|
||||
python scripts/commercial_privacy_artifact_scan.py --json
|
||||
python -m build
|
||||
|
||||
- name: Upload quick quality gate diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: quick-quality-gate-diagnostics
|
||||
path: artifacts/quick-quality-gate.log
|
||||
|
||||
- name: Validate frontend and database contracts
|
||||
run: |
|
||||
npm test --prefix frontend
|
||||
npm run lint --prefix frontend
|
||||
npm run build --prefix frontend
|
||||
|
||||
publish:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/staging'
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and publish API image
|
||||
id: api_build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: deploy/railway-api.Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/jesse-ux/jyotisha-api:${{ github.sha }}
|
||||
|
||||
- name: Build and publish web image
|
||||
id: web_build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: deploy/railway-web.Dockerfile
|
||||
build-args: |
|
||||
NEXT_DEPLOYMENT_ID=${{ github.sha }}
|
||||
push: true
|
||||
tags: ghcr.io/jesse-ux/jyotisha-web:${{ github.sha }}
|
||||
|
||||
- name: Record immutable staging image manifest
|
||||
env:
|
||||
API_DIGEST: ${{ steps.api_build.outputs.digest }}
|
||||
WEB_DIGEST: ${{ steps.web_build.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$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' \
|
||||
"$GITHUB_SHA" "$API_DIGEST" "$WEB_DIGEST" \
|
||||
> artifacts/staging-images/manifest.env
|
||||
node frontend/scripts/staging-image-manifest.mjs \
|
||||
artifacts/staging-images/manifest.env "$GITHUB_SHA" >/dev/null
|
||||
|
||||
- name: Upload immutable staging image manifest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: staging-image-manifest-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: artifacts/staging-images/manifest.env
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -1,68 +0,0 @@
|
||||
name: Jyotish Skill CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
npm ci --prefix frontend
|
||||
|
||||
- name: Print environment diagnostics
|
||||
run: |
|
||||
python --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Run Ruff lint for quality gate files
|
||||
run: 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
|
||||
|
||||
- name: Run Python syntax check
|
||||
run: python -m py_compile scripts/*.py jyotish_vedic/*.py mcp_server.py
|
||||
|
||||
- name: Run quick quality gate
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
python scripts/run_quality_gate.py --profile quick --skip-yoga-logic --skip-frontend-runtime 2>&1 | tee artifacts/quick-quality-gate.log
|
||||
- name: Run commercial privacy artifact gate
|
||||
run: python scripts/commercial_privacy_artifact_scan.py --json
|
||||
- name: Upload quick quality gate diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: quick-quality-gate-diagnostics
|
||||
path: artifacts/
|
||||
|
||||
- name: Validate production web
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder
|
||||
run: |
|
||||
npm test --prefix frontend
|
||||
npm run lint --prefix frontend
|
||||
npm run build --prefix frontend
|
||||
|
||||
- name: Build Python package
|
||||
run: python -m build --no-isolation
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Production deployment moved to Gitea
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
retired:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Refuse deployment from the mirror
|
||||
run: |
|
||||
echo "Production deployment is controlled by .gitea/workflows/deploy-production.yml in git.copse.top." >&2
|
||||
exit 1
|
||||
@@ -1,254 +0,0 @@
|
||||
name: Deploy staging
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Staging Backend Quality Gate"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deploy_sha:
|
||||
description: Exact tested 40-character staging commit SHA
|
||||
required: true
|
||||
type: string
|
||||
allow_rollback:
|
||||
description: Explicitly permit a manual rollback to an older tested SHA
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
packages: read
|
||||
|
||||
concurrency:
|
||||
group: staging-mutation
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'staging')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.STAGING_URL }}
|
||||
env:
|
||||
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: Validate tested revision and gate run
|
||||
id: revision
|
||||
env:
|
||||
REQUESTED_SHA: ${{ github.event.workflow_run.head_sha || inputs.deploy_sha }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
WORKFLOW_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
|
||||
REQUESTED_ROLLBACK: ${{ inputs.allow_rollback || 'false' }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$REQUESTED_SHA" =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo "deploy_sha must be a lowercase full commit SHA" >&2
|
||||
exit 1
|
||||
}
|
||||
allow_rollback=false
|
||||
if [ "$REQUESTED_ROLLBACK" = "true" ]; then
|
||||
[ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] || {
|
||||
echo "rollback authorization is manual-only" >&2
|
||||
exit 1
|
||||
}
|
||||
allow_rollback=true
|
||||
fi
|
||||
|
||||
gate_run_id="$WORKFLOW_RUN_ID"
|
||||
gate_run_attempt="$WORKFLOW_RUN_ATTEMPT"
|
||||
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
runs="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/workflows/backend-quality-gate.yml/runs?head_sha=$REQUESTED_SHA&branch=staging&event=push&status=success&per_page=100")"
|
||||
selected_run="$(jq -cer --arg sha "$REQUESTED_SHA" '
|
||||
[.workflow_runs[] | select(
|
||||
.head_sha == $sha and .head_branch == "staging" and
|
||||
.event == "push" and .conclusion == "success"
|
||||
)] | sort_by(.id) | reverse | first
|
||||
' <<<"$runs")"
|
||||
gate_run_id="$(jq -er '.id' <<<"$selected_run")"
|
||||
gate_run_attempt="$(jq -er '.run_attempt' <<<"$selected_run")"
|
||||
fi
|
||||
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || {
|
||||
echo "no successful exact-SHA staging quality gate run found" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$gate_run_attempt" =~ ^[1-9][0-9]*$ ]] || {
|
||||
echo "invalid staging quality gate run attempt" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
staging_head="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/staging" |
|
||||
jq -er '.object.sha')"
|
||||
if [ "$allow_rollback" = "false" ] && [ "$REQUESTED_SHA" != "$staging_head" ]; then
|
||||
echo "stale staging revision refused; use explicit manual rollback only when intended" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "sha=$REQUESTED_SHA"
|
||||
echo "gate_run_id=$gate_run_id"
|
||||
echo "gate_run_attempt=$gate_run_attempt"
|
||||
echo "allow_rollback=$allow_rollback"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout trusted main controller
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download gate-produced image manifest
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: staging-image-manifest-${{ steps.revision.outputs.sha }}-${{ steps.revision.outputs.gate_run_attempt }}
|
||||
path: artifacts/staging-image
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ steps.revision.outputs.gate_run_id }}
|
||||
|
||||
- name: Validate immutable image manifest
|
||||
id: images
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node frontend/scripts/staging-image-manifest.mjs \
|
||||
artifacts/staging-image/manifest.env "$DEPLOY_SHA" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify reviewed revision and staging target
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git cat-file -e "$DEPLOY_SHA^{commit}"
|
||||
git merge-base --is-ancestor "$DEPLOY_SHA" HEAD || {
|
||||
echo "staging revision is not in the reviewed main history" >&2
|
||||
exit 1
|
||||
}
|
||||
test "$DEPLOY_HOST" = "118.26.111.127"
|
||||
test "$DEPLOY_PORT" = "22"
|
||||
test "$DEPLOY_USER" = "deploy"
|
||||
test "$DEPLOY_PATH" = "/opt/jyotisha-staging"
|
||||
test "$STAGING_URL" = "https://staging.jyotisha.chat"
|
||||
test -n "$STAGING_KNOWN_HOSTS"
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Verify forward-only deployed revision
|
||||
id: previous
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
ALLOW_ROLLBACK: ${{ steps.revision.outputs.allow_rollback }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
||||
previous_sha="$(ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"state='$DEPLOY_PATH/.state/deployed-revision'; if [ -r \"\$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")"
|
||||
if [ "$previous_sha" != "not-deployed" ] && [[ ! "$previous_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "invalid deployed staging revision state" >&2
|
||||
exit 1
|
||||
fi
|
||||
forward_verified=true
|
||||
if [ "$ALLOW_ROLLBACK" = "false" ] &&
|
||||
[ "$previous_sha" != "not-deployed" ] &&
|
||||
[ "$previous_sha" != "$DEPLOY_SHA" ]; then
|
||||
comparison="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/compare/$previous_sha...$DEPLOY_SHA")"
|
||||
jq -e --arg base "$previous_sha" '
|
||||
.status == "ahead" and .merge_base_commit.sha == $base
|
||||
' <<<"$comparison" >/dev/null || {
|
||||
echo "automatic staging rollback or divergent deploy refused" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
{
|
||||
echo "sha=$previous_sha"
|
||||
echo "forward_verified=$forward_verified"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stage trusted controller files in an isolated incoming directory
|
||||
id: incoming
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=20"
|
||||
RSYNC_SSH="ssh $SSH_OPTIONS"
|
||||
incoming="$DEPLOY_PATH/.incoming/$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -d -m 700 '$incoming'"
|
||||
echo "path=$incoming" >>"$GITHUB_OUTPUT"
|
||||
rsync -az --delete --prune-empty-dirs \
|
||||
--include='/deploy/' --include='/deploy/***' --exclude='*' \
|
||||
-e "$RSYNC_SSH" ./ "$DEPLOY_USER@$DEPLOY_HOST:$incoming/"
|
||||
|
||||
- name: Log in to GHCR with run-local Docker state
|
||||
env:
|
||||
GHCR_TOKEN: ${{ github.token }}
|
||||
INCOMING_PATH: ${{ steps.incoming.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -d -m 700 '$INCOMING_PATH/.docker'"
|
||||
printf '%s' "$GHCR_TOKEN" | ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"DOCKER_CONFIG='$INCOMING_PATH/.docker' docker login ghcr.io --username '$GITHUB_ACTOR' --password-stdin"
|
||||
|
||||
- name: Deploy and verify exact image digests under host lock
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
API_IMAGE: ${{ steps.images.outputs.api_image }}
|
||||
WEB_IMAGE: ${{ steps.images.outputs.web_image }}
|
||||
ALLOW_ROLLBACK: ${{ steps.revision.outputs.allow_rollback }}
|
||||
EXPECTED_PREVIOUS_SHA: ${{ steps.previous.outputs.sha }}
|
||||
FORWARD_REVISION_VERIFIED: ${{ steps.previous.outputs.forward_verified }}
|
||||
INCOMING_PATH: ${{ steps.incoming.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=20"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"INCOMING_PATH='$INCOMING_PATH' DEPLOY_PATH='$DEPLOY_PATH' API_IMAGE='$API_IMAGE' WEB_IMAGE='$WEB_IMAGE' DEPLOY_SHA='$DEPLOY_SHA' EXPECTED_PREVIOUS_SHA='$EXPECTED_PREVIOUS_SHA' ALLOW_ROLLBACK='$ALLOW_ROLLBACK' FORWARD_REVISION_VERIFIED='$FORWARD_REVISION_VERIFIED' DOCKER_CONFIG='$INCOMING_PATH/.docker' STAGING_URL='$STAGING_URL' bash '$INCOMING_PATH/deploy/run-staging-deploy.sh'" |
|
||||
tee staging-deploy-result.txt
|
||||
sed 's/^/- /' staging-deploy-result.txt >>"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Remove run-local staging files
|
||||
if: always() && steps.incoming.outputs.path != ''
|
||||
continue-on-error: true
|
||||
env:
|
||||
INCOMING_PATH: ${{ steps.incoming.outputs.path }}
|
||||
run: |
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"DOCKER_CONFIG='$INCOMING_PATH/.docker' docker logout ghcr.io >/dev/null 2>&1 || true; rm -rf -- '$INCOMING_PATH'"
|
||||
@@ -1,234 +0,0 @@
|
||||
name: Migrate Staging Database
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deploy_sha:
|
||||
description: Full tested staging commit SHA to migrate
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: staging-mutation
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
migrate:
|
||||
environment: staging
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DEPLOY_HOST: ${{ vars.STAGING_HOST }}
|
||||
DEPLOY_PORT: ${{ vars.STAGING_PORT }}
|
||||
DEPLOY_USER: ${{ vars.STAGING_USER }}
|
||||
DEPLOY_PATH: ${{ vars.STAGING_PATH }}
|
||||
STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }}
|
||||
|
||||
steps:
|
||||
- name: Validate current tested staging revision
|
||||
id: revision
|
||||
env:
|
||||
REQUESTED_SHA: ${{ inputs.deploy_sha }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$REQUESTED_SHA" =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo "deploy_sha must be a lowercase full commit SHA" >&2
|
||||
exit 1
|
||||
}
|
||||
runs="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/workflows/backend-quality-gate.yml/runs?head_sha=$REQUESTED_SHA&branch=staging&event=push&status=success&per_page=100")"
|
||||
selected_run="$(jq -cer --arg sha "$REQUESTED_SHA" '
|
||||
[.workflow_runs[] | select(
|
||||
.head_sha == $sha and .head_branch == "staging" and
|
||||
.event == "push" and .conclusion == "success"
|
||||
)] | sort_by(.id) | reverse | first
|
||||
' <<<"$runs")"
|
||||
gate_run_id="$(jq -er '.id' <<<"$selected_run")"
|
||||
gate_run_attempt="$(jq -er '.run_attempt' <<<"$selected_run")"
|
||||
[[ "$gate_run_id" =~ ^[0-9]+$ ]]
|
||||
[[ "$gate_run_attempt" =~ ^[1-9][0-9]*$ ]]
|
||||
staging_head="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/staging" |
|
||||
jq -er '.object.sha')"
|
||||
[ "$REQUESTED_SHA" = "$staging_head" ] || {
|
||||
echo "stale staging migration refused; migrate the current staging head" >&2
|
||||
exit 1
|
||||
}
|
||||
{
|
||||
echo "sha=$REQUESTED_SHA"
|
||||
echo "gate_run_id=$gate_run_id"
|
||||
echo "gate_run_attempt=$gate_run_attempt"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout trusted main controller
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download gate-produced image manifest
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: staging-image-manifest-${{ steps.revision.outputs.sha }}-${{ steps.revision.outputs.gate_run_attempt }}
|
||||
path: artifacts/staging-image
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ steps.revision.outputs.gate_run_id }}
|
||||
|
||||
- name: Validate immutable migration image
|
||||
id: images
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node frontend/scripts/staging-image-manifest.mjs \
|
||||
artifacts/staging-image/manifest.env "$DEPLOY_SHA" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify reviewed revision and staging target
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git cat-file -e "$DEPLOY_SHA^{commit}"
|
||||
git merge-base --is-ancestor "$DEPLOY_SHA" HEAD || {
|
||||
echo "staging revision is not in the reviewed main history" >&2
|
||||
exit 1
|
||||
}
|
||||
test "$DEPLOY_HOST" = "118.26.111.127"
|
||||
test "$DEPLOY_PORT" = "22"
|
||||
test "$DEPLOY_USER" = "deploy"
|
||||
test "$DEPLOY_PATH" = "/opt/jyotisha-staging"
|
||||
test -n "$STAGING_KNOWN_HOSTS"
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Verify forward-only migration revision
|
||||
id: previous
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
||||
previous_sha="$(ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"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")"
|
||||
if [ "$previous_sha" != "not-deployed" ] && [[ ! "$previous_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "invalid deployed staging revision state" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$previous_sha" != "not-deployed" ] && [ "$previous_sha" != "$DEPLOY_SHA" ]; then
|
||||
comparison="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/compare/$previous_sha...$DEPLOY_SHA")"
|
||||
jq -e --arg base "$previous_sha" '
|
||||
.status == "ahead" and .merge_base_commit.sha == $base
|
||||
' <<<"$comparison" >/dev/null || {
|
||||
echo "stale or divergent staging migration refused" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
{
|
||||
echo "sha=$previous_sha"
|
||||
echo "forward_verified=true"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stage trusted controller files in an isolated incoming directory
|
||||
id: incoming
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=20"
|
||||
RSYNC_SSH="ssh $SSH_OPTIONS"
|
||||
incoming="$DEPLOY_PATH/.incoming/$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -d -m 700 '$incoming'"
|
||||
echo "path=$incoming" >>"$GITHUB_OUTPUT"
|
||||
rsync -az --delete --prune-empty-dirs \
|
||||
--include='/deploy/' --include='/deploy/***' --exclude='*' \
|
||||
-e "$RSYNC_SSH" ./ "$DEPLOY_USER@$DEPLOY_HOST:$incoming/"
|
||||
|
||||
- name: Log in to GHCR with run-local Docker state
|
||||
env:
|
||||
GHCR_TOKEN: ${{ github.token }}
|
||||
INCOMING_PATH: ${{ steps.incoming.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -d -m 700 '$INCOMING_PATH/.docker'"
|
||||
printf '%s' "$GHCR_TOKEN" | ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"DOCKER_CONFIG='$INCOMING_PATH/.docker' docker login ghcr.io --username '$GITHUB_ACTOR' --password-stdin"
|
||||
|
||||
- name: Apply exact-image migrations under host lock
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
WEB_IMAGE: ${{ steps.images.outputs.web_image }}
|
||||
EXPECTED_PREVIOUS_SHA: ${{ steps.previous.outputs.sha }}
|
||||
FORWARD_REVISION_VERIFIED: ${{ steps.previous.outputs.forward_verified }}
|
||||
INCOMING_PATH: ${{ steps.incoming.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=20"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"INCOMING_PATH='$INCOMING_PATH' DEPLOY_PATH='$DEPLOY_PATH' WEB_IMAGE='$WEB_IMAGE' DEPLOY_SHA='$DEPLOY_SHA' EXPECTED_PREVIOUS_SHA='$EXPECTED_PREVIOUS_SHA' FORWARD_REVISION_VERIFIED='$FORWARD_REVISION_VERIFIED' DOCKER_CONFIG='$INCOMING_PATH/.docker' bash '$INCOMING_PATH/deploy/run-staging-migration.sh'"
|
||||
|
||||
- name: Dispatch current exact-SHA staging deployment
|
||||
env:
|
||||
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
staging_head="$(curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/staging" |
|
||||
jq -er '.object.sha')"
|
||||
[ "$DEPLOY_SHA" = "$staging_head" ] || {
|
||||
echo "staging advanced during migration; refusing stale deployment dispatch" >&2
|
||||
exit 1
|
||||
}
|
||||
payload="$(jq -cn --arg deploy_sha "$DEPLOY_SHA" \
|
||||
'{ref:"main",inputs:{deploy_sha:$deploy_sha,allow_rollback:"false"}}')"
|
||||
curl --fail --silent --show-error --request POST \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header "Accept: application/vnd.github+json" \
|
||||
--header "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$payload" \
|
||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/workflows/deploy-staging.yml/dispatches"
|
||||
|
||||
- name: Remove run-local staging files
|
||||
if: always() && steps.incoming.outputs.path != ''
|
||||
continue-on-error: true
|
||||
env:
|
||||
INCOMING_PATH: ${{ steps.incoming.outputs.path }}
|
||||
run: |
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"DOCKER_CONFIG='$INCOMING_PATH/.docker' docker logout ghcr.io >/dev/null 2>&1 || true; rm -rf -- '$INCOMING_PATH'"
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install build tools
|
||||
run: pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Check package metadata
|
||||
run: twine check dist/*
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
skip-existing: true
|
||||
@@ -1,54 +0,0 @@
|
||||
name: Jyotish Release Quality Gate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-quality-gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
python -m pip install playwright
|
||||
python -m playwright install --with-deps chromium
|
||||
npm ci --prefix frontend
|
||||
|
||||
- name: Print environment diagnostics
|
||||
run: |
|
||||
python --version
|
||||
node --version
|
||||
npm --version
|
||||
npm --prefix frontend exec -- next --version
|
||||
|
||||
- name: Run release quality gate
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
python scripts/run_quality_gate.py --profile release 2>&1 | tee artifacts/release-quality-gate.log
|
||||
|
||||
- name: Upload release quality gate diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-quality-gate-diagnostics
|
||||
path: artifacts/
|
||||
@@ -1,82 +0,0 @@
|
||||
name: Reset Staging Account
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expected_deploy_sha:
|
||||
description: Exact 40-character SHA currently deployed to staging
|
||||
required: true
|
||||
type: string
|
||||
email:
|
||||
description: Exact staging account email
|
||||
required: true
|
||||
type: string
|
||||
confirmation:
|
||||
description: Type RESET followed by a space and the exact email
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: staging-mutation
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
reset:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.STAGING_URL }}
|
||||
env:
|
||||
DEPLOY_HOST: ${{ vars.STAGING_HOST }}
|
||||
DEPLOY_PORT: ${{ vars.STAGING_PORT }}
|
||||
DEPLOY_USER: ${{ vars.STAGING_USER }}
|
||||
DEPLOY_PATH: ${{ vars.STAGING_PATH }}
|
||||
STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }}
|
||||
EXPECTED_DEPLOY_SHA: ${{ inputs.expected_deploy_sha }}
|
||||
RESET_EMAIL: ${{ inputs.email }}
|
||||
RESET_CONFIRMATION: ${{ inputs.confirmation }}
|
||||
|
||||
steps:
|
||||
- name: Checkout trusted controller
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate account reset request and staging target
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]]
|
||||
test "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL"
|
||||
test "$DEPLOY_HOST" = "118.26.111.127"
|
||||
test "$DEPLOY_PORT" = "22"
|
||||
test "$DEPLOY_USER" = "deploy"
|
||||
test "$DEPLOY_PATH" = "/opt/jyotisha-staging"
|
||||
test -n "$STAGING_KNOWN_HOSTS"
|
||||
bash -n deploy/reset-staging-account.sh
|
||||
|
||||
- name: Configure pinned staging SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$SSH_PRIVATE_KEY_BASE64"
|
||||
install -d -m 700 ~/.ssh
|
||||
printf '%s' "$SSH_PRIVATE_KEY_BASE64" | base64 --decode >~/.ssh/jyotisha-staging
|
||||
chmod 600 ~/.ssh/jyotisha-staging
|
||||
ssh-keygen -y -f ~/.ssh/jyotisha-staging >/dev/null
|
||||
printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Reset one staging account under host lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=10"
|
||||
ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"DEPLOY_PATH='$DEPLOY_PATH' EXPECTED_DEPLOY_SHA='$EXPECTED_DEPLOY_SHA' RESET_EMAIL='$RESET_EMAIL' RESET_CONFIRMATION='$RESET_CONFIRMATION' bash -s" \
|
||||
< deploy/reset-staging-account.sh
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Jyotish Skill Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with: { python-version: '3.11' }
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
npm ci --prefix frontend
|
||||
- name: Print environment diagnostics
|
||||
run: |
|
||||
python --version
|
||||
node --version
|
||||
npm --version
|
||||
- name: Run pytest suite
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
python -m pytest -vv --maxfail=1 --junitxml=artifacts/pytest.xml 2>&1 | tee artifacts/pytest.log
|
||||
- name: Upload pytest diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pytest-diagnostics
|
||||
path: artifacts/
|
||||
- name: Run legacy runner
|
||||
run: python tests/run_all.py
|
||||
- name: Validate production web
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder
|
||||
run: |
|
||||
npm test --prefix frontend
|
||||
npm run lint --prefix frontend
|
||||
npm run build --prefix frontend
|
||||
Reference in New Issue
Block a user