Files
Jyotisha/.gitea/workflows/migrate-staging-database.yml
T
linmeng 772c06c205
Deploy staging to test server / deploy (push) Successful in 2m27s
fix: use deploy-owned staging temp paths
Create staging mutation payloads in randomized deploy-owned temporary directories so migration and deployment do not require writes under the protected application root.
2026-07-30 11:11:58 +08:00

94 lines
6.1 KiB
YAML

name: Migrate Staging Database (manual only)
on:
workflow_dispatch:
inputs:
deploy_sha:
description: Full current staging SHA to migrate
required: true
type: string
concurrency:
group: staging-mutation
cancel-in-progress: false
jobs:
migrate:
runs-on: xiaoxin
timeout-minutes: 20
env:
GITEA_SHA: ${{ gitea.sha }}
REGISTRY_HOST: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com
IMAGE_REPOSITORY: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha
DEPLOY_HOST: ${{ vars.STAGING_HOST }}
DEPLOY_PORT: ${{ vars.STAGING_PORT }}
DEPLOY_USER: ${{ vars.STAGING_USER }}
DEPLOY_PATH: ${{ vars.STAGING_PATH }}
STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }}
steps:
- name: Checkout staging controller
run: |
set -euo pipefail
git init .
git remote remove origin 2>/dev/null || true
git remote add origin https://git.copse.top/root/Jyotisha.git
git fetch --no-tags origin staging
git checkout --detach --force origin/staging
- name: Verify runner toolchain
run: |
set -euo pipefail
python3 --version
node --version
npm --version
docker version
- name: Validate revision and resolve migration image
id: image
env:
DEPLOY_SHA: ${{ inputs.deploy_sha }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
[[ "$DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1
[[ "$(git ls-remote origin refs/heads/staging | awk '{print $1}')" == "$DEPLOY_SHA" ]] || { echo "migration requires current staging head" >&2; exit 1; }
git cat-file -e "${DEPLOY_SHA}^{commit}"
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin
web_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c '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 "")')"
[[ "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1
echo "web_image=${IMAGE_REPOSITORY}@${web_digest}" >> "$GITHUB_OUTPUT"
echo "deploy_sha=$DEPLOY_SHA" >> "$GITHUB_OUTPUT"
- name: Apply digest-pinned migration under host lock
env:
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DEPLOY_SHA: ${{ steps.image.outputs.deploy_sha }}
WEB_IMAGE: ${{ steps.image.outputs.web_image }}
run: |
set -euo pipefail
ssh_root="${RUNNER_TEMP}/staging-migration-ssh"
key_path="$ssh_root/id_ed25519"
known_hosts_path="$ssh_root/known_hosts"
install -m 700 -d "$ssh_root"
printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_path"
printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$known_hosts_path"
chmod 600 "$key_path" "$known_hosts_path"
ssh_options=(-i "$key_path" -p "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path")
remote="$DEPLOY_USER@$DEPLOY_HOST"
incoming="$(ssh "${ssh_options[@]}" "$remote" "mktemp -d /tmp/jyotisha-staging.XXXXXXXXXX")"
[[ "$incoming" == /tmp/jyotisha-staging.* ]]
cleanup() { ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker logout '$REGISTRY_HOST' >/dev/null 2>&1 || true; rm -rf -- '$incoming'" >/dev/null 2>&1 || true; docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true; rm -rf -- "$ssh_root"; }
trap cleanup EXIT
ssh "${ssh_options[@]}" "$remote" "install -d -m 700 '$incoming/.docker'"
tar -cf "${RUNNER_TEMP}/deploy.tar" deploy
scp -i "$key_path" -P "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path" "${RUNNER_TEMP}/deploy.tar" "$remote:$incoming/deploy.tar"
ssh "${ssh_options[@]}" "$remote" "tar -xf '$incoming/deploy.tar' -C '$incoming' && rm -f -- '$incoming/deploy.tar'"
previous_sha="$(ssh "${ssh_options[@]}" "$remote" "state='$DEPLOY_PATH/.state/deployed-revision'; if [ -f \"\$state\" ]; then cat \"\$state\"; else id=\$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1); if [ -n \"\$id\" ]; then docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \"\$id\" | sed -n 's/^GITHUB_SHA=//p' | head -n 1; else printf not-deployed; fi; fi")"
[[ "$previous_sha" == not-deployed || "$previous_sha" =~ ^[0-9a-f]{40}$ ]] || exit 1
forward_verified=false
if [[ "$previous_sha" != not-deployed && "$previous_sha" != "$DEPLOY_SHA" ]]; then git cat-file -e "${previous_sha}^{commit}" 2>/dev/null || git fetch origin "$previous_sha"; git merge-base --is-ancestor "$previous_sha" "$DEPLOY_SHA" || { echo "migration rollback or divergence refused" >&2; exit 1; }; forward_verified=true; fi
printf '%s' "$REGISTRY_PASSWORD" | ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker login '$REGISTRY_HOST' --username '$REGISTRY_USERNAME' --password-stdin"
ssh "${ssh_options[@]}" "$remote" "INCOMING_PATH='$incoming' DEPLOY_PATH='$DEPLOY_PATH' WEB_IMAGE='$WEB_IMAGE' DEPLOY_SHA='$DEPLOY_SHA' EXPECTED_PREVIOUS_SHA='$previous_sha' FORWARD_REVISION_VERIFIED='$forward_verified' DOCKER_CONFIG='$incoming/.docker' bash '$incoming/deploy/run-staging-migration.sh'"
- name: Operator action
run: echo 'Migration complete. Start Deploy staging manually with this exact SHA.'