diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..f33ab3c --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: CD + +# Every merge into dev โ†’ lint + tests โ†’ deploy to the shared server (dev.meditron.tenvex.ru / meditron.tenvex.ru). +# Secrets: DEPLOY_SSH_KEY (private key of the `ptsr-deploy` user), DEPLOY_HOST (user@host). +on: + push: + branches: [dev] + workflow_dispatch: + +concurrency: + group: deploy-dev + cancel-in-progress: false + +jobs: + test: + name: ruff ยท pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v3 + with: + version: "0.8.4" + - run: uv sync --frozen + - run: uv run ruff check . + - run: uv run pytest -q + + deploy: + name: deploy dev + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: ssh key + run: | + install -d -m 700 ~/.ssh + printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + - name: deploy + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_SSH_OPTS: "-i /root/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=20" + run: | + export DEPLOY_SSH_OPTS="${DEPLOY_SSH_OPTS//\/root\/.ssh/$HOME/.ssh}" + infra/deploy/deploy.sh HEAD + - name: smoke + run: | + sleep 10 + for u in https://dev.meditron.tenvex.ru/ https://dev.meditron.tenvex.ru/api/v1/demo/personas https://dev.meditron.tenvex.ru/doctor/; do + code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$u"); echo "$u -> $code"; [ "$code" = 200 ] + done diff --git a/infra/deploy/deploy.sh b/infra/deploy/deploy.sh index adc9b28..8dbae52 100755 --- a/infra/deploy/deploy.sh +++ b/infra/deploy/deploy.sh @@ -1,24 +1,37 @@ #!/usr/bin/env bash -# Deploy origin/dev to the shared server (ssh alias `vobot`, dir /srv/ptsr). Run from a clean checkout: +# Deploy a git ref to the shared server (dir /srv/ptsr). Used by hand and by CD (.gitea/workflows/deploy.yml). # infra/deploy/deploy.sh [git-ref] default: origin/dev +# Env: DEPLOY_HOST (ssh host/alias, default `vobot`), DEPLOY_SSH_OPTS (extra ssh/scp options). # Keeps /srv/ptsr/.env (secrets live only on the server) and the Postgres volume. +# The host drops some ssh connections (rate limiting) โ€” every remote step is retried. set -euo pipefail REF=${1:-origin/dev} HOST=${DEPLOY_HOST:-vobot} +OPTS=${DEPLOY_SSH_OPTS:-} DIR=/srv/ptsr -git fetch -q origin || true +git fetch -q origin 2>/dev/null || true SHA=$(git rev-parse --short "$REF") +TGZ=$(mktemp --suffix=.tgz) +trap 'rm -f "$TGZ"' EXIT +git archive --format=tar.gz -o "$TGZ" "$REF" + +retry() { local n=0; until "$@"; do n=$((n + 1)); [ $n -ge 5 ] && return 1; echo "retry $n: $1" >&2; sleep $((n * 10)); done; } + echo "deploy $REF ($SHA) -> $HOST:$DIR" -git archive --format=tar "$REF" | ssh "$HOST" "rm -rf $DIR/app.new && mkdir -p $DIR/app.new && tar -x -C $DIR/app.new" -ssh "$HOST" bash -s < REVISION -docker run --rm -v "\$PWD/web:/w" -w /w node:20-alpine sh -c "npm ci --no-audit --no-fund >/dev/null && npm run build >/dev/null" +docker run --rm --user "\$(id -u):\$(id -g)" -e HOME=/tmp -e npm_config_cache=/tmp/.npm -v "\$PWD/web:/w" -w /w node:20-alpine sh -c "npm ci --no-audit --no-fund >/dev/null && npm run build >/dev/null" docker compose -p ptsr -f docker-compose.yml -f infra/deploy/docker-compose.server.yml up -d --build /dev/null && break; sleep 3; done -curl -sf 127.0.0.1:8010/health && echo " api ok" +curl -sf 127.0.0.1:8010/health >/dev/null && echo "api ok @ $SHA" SH