Files
mostovik-backend/.gitea/workflows/ci-cd.yml
Aleksandr Meshchriakov f55e74c78e
Some checks failed
Mostovik Backend CI/CD / Tests and lint (push) Successful in 4m12s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Failing after 3m48s
Mostovik Backend CI/CD / Deploy dev (push) Has been skipped
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
ci: tolerate missing Gitea run attempt
2026-08-27 13:49:36 +03:00

325 lines
14 KiB
YAML

name: Mostovik Backend CI/CD
on:
push:
branches:
- dev
# Add main only after the first manual main restore/rollback drill passes.
pull_request:
branches:
- dev
- main
workflow_dispatch:
concurrency:
group: mostovik-backend-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
REGISTRY_HOST: registry.dev.nii-ecos.ru
REGISTRY_NAMESPACE: ${{ github.repository_owner }}
WEB_IMAGE: mostovik-backend-web
CELERY_IMAGE: mostovik-backend-celery
CUSTOMER_DEPLOY_HOST: 10.0.10.174
CUSTOMER_DEPLOY_USER: ecos
CUSTOMER_SSH_PROXY_HOST: ecos-proxy@10.10.0.121
jobs:
quality:
name: Tests and lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout exact event revision
uses: actions/checkout@v4.2.2
with:
persist-credentials: false
- name: Run quality gate
run: |
set -euo pipefail
ci_image="mostovik-backend-ci:${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
docker build \
--file docker/Dockerfile \
--target ci-deps-base \
--tag "${ci_image}" \
.
tar --exclude=.git --exclude=.venv --exclude=.ruff_cache -cf - . \
| docker run --rm -i \
--env DJANGO_SETTINGS_MODULE=settings.test \
--env SECRET_KEY=test-secret-key-for-ci \
"${ci_image}" \
bash -c '
set -euo pipefail
mkdir -p /workspace
tar -xf - -C /workspace
cd /workspace
export PATH="/app/.venv/bin:${PATH}"
export PYTHONPATH="/workspace/src:${PYTHONPATH:-}"
ruff check src
ruff format src --check
python -m pytest tests --ignore=tests/test_api_inventory_e2e.py -q
python -m pytest tests/test_api_inventory_e2e.py -q
'
build:
name: Build linux/amd64 release images
runs-on: [backend-docker]
timeout-minutes: 60
needs: [quality]
if: ${{ needs.quality.result == 'success' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') }}
outputs:
web_ref: ${{ steps.release.outputs.web_ref }}
celery_ref: ${{ steps.release.outputs.celery_ref }}
web_tag_ref: ${{ steps.release.outputs.web_tag_ref }}
celery_tag_ref: ${{ steps.release.outputs.celery_tag_ref }}
steps:
- name: Checkout exact event revision
uses: actions/checkout@v4.2.2
with:
persist-credentials: false
- name: Build once and publish immutable digests
id: release
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_PASSWORD:-}" ]; then
echo "REGISTRY_USERNAME and REGISTRY_PASSWORD secrets are required" >&2
exit 1
fi
printf '%s' "${REGISTRY_PASSWORD}" \
| docker login "${REGISTRY_HOST}" --username "${REGISTRY_USERNAME}" --password-stdin
builder="mostovik-backend-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
docker buildx create --name "${builder}" --driver docker-container --use
cleanup() {
docker buildx rm "${builder}" >/dev/null 2>&1 || true
rm -f web-metadata.json celery-metadata.json
}
trap cleanup EXIT
docker buildx inspect --bootstrap
sha_short="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)"
image_tag="${GITHUB_REF_NAME}-${sha_short}"
registry_path="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}"
web_repository="${registry_path}/${WEB_IMAGE}"
celery_repository="${registry_path}/${CELERY_IMAGE}"
build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker buildx build \
--platform linux/amd64 \
--file docker/Dockerfile \
--target runtime-web \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.created=${build_time}" \
--metadata-file web-metadata.json \
--push \
--tag "${web_repository}:${image_tag}" \
.
docker buildx build \
--platform linux/amd64 \
--file docker/Dockerfile \
--target runtime-celery \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.created=${build_time}" \
--metadata-file celery-metadata.json \
--push \
--tag "${celery_repository}:${image_tag}" \
.
web_digest="$(jq -r '."containerimage.digest" // empty' web-metadata.json)"
celery_digest="$(jq -r '."containerimage.digest" // empty' celery-metadata.json)"
printf '%s\n' "${web_digest}" | grep -Eq '^sha256:[0-9a-f]{64}$'
printf '%s\n' "${celery_digest}" | grep -Eq '^sha256:[0-9a-f]{64}$'
{
echo "web_ref=${web_repository}@${web_digest}"
echo "celery_ref=${celery_repository}@${celery_digest}"
echo "web_tag_ref=${web_repository}:${image_tag}"
echo "celery_tag_ref=${celery_repository}:${image_tag}"
} >> "${GITHUB_OUTPUT}"
deploy_dev:
name: Deploy dev
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [build]
if: ${{ github.ref == 'refs/heads/dev' && needs.build.result == 'success' }}
steps:
- name: Invoke dev release wrapper
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
INTERNAL_DEPLOY_SSH_KEY_B64: ${{ secrets.INTERNAL_DEPLOY_SSH_KEY_B64 }}
INTERNAL_KNOWN_HOSTS_B64: ${{ secrets.INTERNAL_KNOWN_HOSTS_B64 }}
WEB_REF: ${{ needs.build.outputs.web_ref }}
CELERY_REF: ${{ needs.build.outputs.celery_ref }}
run: |
set -euo pipefail
if [ -z "${DEPLOY_HOST:-}" ] || [ -z "${DEPLOY_USER:-}" ] || [ -z "${INTERNAL_DEPLOY_SSH_KEY_B64:-}" ] || [ -z "${INTERNAL_KNOWN_HOSTS_B64:-}" ]; then
echo "Internal deploy SSH secrets are required" >&2
exit 1
fi
case "${DEPLOY_USER}" in *[!A-Za-z0-9._-]*) echo "DEPLOY_USER has unsupported characters" >&2; exit 1;; esac
case "${DEPLOY_HOST}" in *[!A-Za-z0-9.:-]*) echo "DEPLOY_HOST has unsupported characters" >&2; exit 1;; esac
printf '%s\n' "${WEB_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
printf '%s\n' "${CELERY_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
ssh_dir="$(mktemp -d)"
trap 'rm -rf "${ssh_dir}"' EXIT
key_path="${ssh_dir}/deploy_key"
known_hosts_path="${ssh_dir}/known_hosts"
printf '%s' "${INTERNAL_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
printf '%s' "${INTERNAL_KNOWN_HOSTS_B64}" | base64 -d > "${known_hosts_path}"
chmod 600 "${key_path}" "${known_hosts_path}"
ssh-keygen -y -f "${key_path}" >/dev/null
ssh \
-i "${key_path}" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
-o "UserKnownHostsFile=${known_hosts_path}" \
-o GlobalKnownHostsFile=/dev/null \
-o ConnectTimeout=15 \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
/opt/ecos-dev/release-service \
mostovik-backend \
"${GITHUB_RUN_ID:-${GITHUB_SHA}}-${GITHUB_RUN_ATTEMPT:-1}" \
"${WEB_REF}" \
"${CELERY_REF}"
deploy_internal_main:
name: Deploy and verify internal main
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [build]
if: ${{ github.ref == 'refs/heads/main' && needs.build.result == 'success' }}
steps:
- name: Refresh clone and invoke internal-main release wrapper
env:
MAIN_DEPLOY_HOST: ${{ secrets.MAIN_DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
INTERNAL_DEPLOY_SSH_KEY_B64: ${{ secrets.INTERNAL_DEPLOY_SSH_KEY_B64 }}
INTERNAL_KNOWN_HOSTS_B64: ${{ secrets.INTERNAL_KNOWN_HOSTS_B64 }}
INTERNAL_MAIN_MOSTOVIK_BACKEND_URL: ${{ secrets.INTERNAL_MAIN_MOSTOVIK_BACKEND_URL }}
WEB_REF: ${{ needs.build.outputs.web_ref }}
CELERY_REF: ${{ needs.build.outputs.celery_ref }}
run: |
set -euo pipefail
if [ -z "${MAIN_DEPLOY_HOST:-}" ] || [ -z "${DEPLOY_USER:-}" ] || [ -z "${INTERNAL_DEPLOY_SSH_KEY_B64:-}" ] || [ -z "${INTERNAL_KNOWN_HOSTS_B64:-}" ] || [ -z "${INTERNAL_MAIN_MOSTOVIK_BACKEND_URL:-}" ]; then
echo "Internal-main deploy and smoke secrets are required" >&2
exit 1
fi
case "${DEPLOY_USER}" in *[!A-Za-z0-9._-]*) echo "DEPLOY_USER has unsupported characters" >&2; exit 1;; esac
case "${MAIN_DEPLOY_HOST}" in *[!A-Za-z0-9.:-]*) echo "MAIN_DEPLOY_HOST has unsupported characters" >&2; exit 1;; esac
printf '%s\n' "${WEB_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
printf '%s\n' "${CELERY_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
ssh_dir="$(mktemp -d)"
trap 'rm -rf "${ssh_dir}"' EXIT
key_path="${ssh_dir}/deploy_key"
known_hosts_path="${ssh_dir}/known_hosts"
printf '%s' "${INTERNAL_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
printf '%s' "${INTERNAL_KNOWN_HOSTS_B64}" | base64 -d > "${known_hosts_path}"
chmod 600 "${key_path}" "${known_hosts_path}"
ssh-keygen -y -f "${key_path}" >/dev/null
ssh \
-i "${key_path}" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
-o "UserKnownHostsFile=${known_hosts_path}" \
-o GlobalKnownHostsFile=/dev/null \
-o ConnectTimeout=15 \
"${DEPLOY_USER}@${MAIN_DEPLOY_HOST}" \
/opt/ecos-main/release-service \
mostovik-backend \
"${GITHUB_RUN_ID:-${GITHUB_SHA}}-${GITHUB_RUN_ATTEMPT:-1}" \
"${WEB_REF}" \
"${CELERY_REF}" \
--refresh-data
smoke_url="${INTERNAL_MAIN_MOSTOVIK_BACKEND_URL%/}/health/ready/"
for attempt in $(seq 1 30); do
if curl --fail --silent --output /dev/null --max-time 10 "${smoke_url}"; then
echo "Internal-main backend readiness passed"
exit 0
fi
echo "Internal-main backend readiness attempt ${attempt}/30 failed"
sleep 5
done
echo "Internal-main backend readiness failed" >&2
exit 1
deploy_customer_main:
name: Deploy customer main
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [build, deploy_internal_main]
if: ${{ github.ref == 'refs/heads/main' && needs.build.result == 'success' && needs.deploy_internal_main.result == 'success' }}
steps:
- name: Invoke customer release wrapper with the verified digests
env:
CUSTOMER_DEPLOY_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }}
CUSTOMER_KNOWN_HOSTS_B64: ${{ secrets.CUSTOMER_KNOWN_HOSTS_B64 }}
WEB_REF: ${{ needs.build.outputs.web_ref }}
CELERY_REF: ${{ needs.build.outputs.celery_ref }}
run: |
set -euo pipefail
for name in CUSTOMER_DEPLOY_HOST CUSTOMER_DEPLOY_USER CUSTOMER_SSH_PROXY_HOST CUSTOMER_DEPLOY_SSH_KEY_B64 CUSTOMER_KNOWN_HOSTS_B64; do
if [ -z "${!name:-}" ]; then
echo "Missing required customer SSH secret: ${name}" >&2
exit 1
fi
done
case "${CUSTOMER_DEPLOY_USER}" in *[!A-Za-z0-9._-]*) echo "Customer SSH user has unsupported characters" >&2; exit 1;; esac
case "${CUSTOMER_DEPLOY_HOST}" in *[!A-Za-z0-9.:-]*) echo "Customer SSH host has unsupported characters" >&2; exit 1;; esac
case "${CUSTOMER_SSH_PROXY_HOST}" in *[!A-Za-z0-9._@:-]*) echo "Customer SSH proxy has unsupported characters" >&2; exit 1;; esac
printf '%s\n' "${WEB_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
printf '%s\n' "${CELERY_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
ssh_dir="$(mktemp -d)"
trap 'rm -rf "${ssh_dir}"' EXIT
key_path="${ssh_dir}/deploy_key"
known_hosts_path="${ssh_dir}/known_hosts"
printf '%s' "${CUSTOMER_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
printf '%s' "${CUSTOMER_KNOWN_HOSTS_B64}" | base64 -d > "${known_hosts_path}"
chmod 600 "${key_path}" "${known_hosts_path}"
ssh-keygen -y -f "${key_path}" >/dev/null
ssh_common=(
-i "${key_path}"
-o BatchMode=yes
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
-o "UserKnownHostsFile=${known_hosts_path}"
-o GlobalKnownHostsFile=/dev/null
-o ConnectTimeout=15
)
proxy_command="ssh -i ${key_path} -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${known_hosts_path} -o GlobalKnownHostsFile=/dev/null -o ConnectTimeout=15 -W %h:%p ${CUSTOMER_SSH_PROXY_HOST}"
ssh \
"${ssh_common[@]}" \
-o "ProxyCommand=${proxy_command}" \
"${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" \
/ecos/release-service \
mostovik-backend \
"${GITHUB_RUN_ID:-${GITHUB_SHA}}-${GITHUB_RUN_ATTEMPT:-1}" \
"${WEB_REF}" \
"${CELERY_REF}"