name: State Corp 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: state-corp-backend-${{ github.ref }} cancel-in-progress: false env: PYTHON_VERSION: "3.11" REGISTRY_HOST: registry.dev.nii-ecos.ru REGISTRY_NAMESPACE: avm WEB_IMAGE: state-corp-backend-web CELERY_IMAGE: state-corp-backend-celery jobs: quality: name: Quality gate runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4.2.2 with: persist-credentials: false - name: Validate release client contract run: ./scripts/ci/release-service-test.sh - name: Install Python and uv run: | set -euo pipefail project_python_version="$(cat .python-version 2>/dev/null || printf '%s' "${PYTHON_VERSION}")" python_bin="$(./scripts/ensure-ci-python.sh "${project_python_version}")" printf 'PYTHON_BIN=%s\n' "${python_bin}" > .ci-python-env - name: Install dependencies run: | set -euo pipefail . ./.ci-python-env "${PYTHON_BIN}" -m venv .venv . .venv/bin/activate python -m pip install --upgrade pip uv uv sync \ --dev \ --frozen \ --active \ --python "${PYTHON_BIN}" \ --no-managed-python \ --no-python-downloads - name: Lint and formatting run: | set -euo pipefail . .venv/bin/activate ruff check src tests scripts ruff format src tests scripts --check - name: Tests env: DJANGO_SETTINGS_MODULE: settings.test SECRET_KEY: test-secret-key-for-ci run: | set -euo pipefail export PYTHONPATH="${PWD}/src:${PYTHONPATH:-}" .venv/bin/python -m pytest tests -q build: name: Build linux/amd64 images once runs-on: [backend-docker] timeout-minutes: 60 needs: [quality] if: >- github.event_name != 'pull_request' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') outputs: celery_ref: ${{ steps.image_refs.outputs.celery_ref }} celery_tag_ref: ${{ steps.image_refs.outputs.celery_tag_ref }} run_id: ${{ steps.image_refs.outputs.run_id }} web_ref: ${{ steps.image_refs.outputs.web_ref }} web_tag_ref: ${{ steps.image_refs.outputs.web_tag_ref }} steps: - name: Checkout uses: actions/checkout@v4.2.2 with: persist-credentials: false - name: Validate registry credentials env: REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} run: | set -euo pipefail [ -n "${REGISTRY_USERNAME:-}" ] || { echo 'Registry username is not configured.' >&2 exit 1 } [ -n "${REGISTRY_PASSWORD:-}" ] || { echo 'Registry password/token is not configured.' >&2 exit 1 } - name: Prepare Buildx run: | set -euo pipefail builder_name="state-corp-backend-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}" docker buildx create \ --driver docker-container \ --name "${builder_name}" \ --use docker buildx inspect --bootstrap printf 'BUILDER_NAME=%s\n' "${builder_name}" >> "${GITHUB_ENV}" - name: Build and push immutable images id: image_refs env: REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} run: | set -euo pipefail registry_path="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}" web_image="${registry_path}/${WEB_IMAGE}" celery_image="${registry_path}/${CELERY_IMAGE}" short_sha="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)" branch="${GITHUB_REF_NAME}" immutable_tag="${branch}-${short_sha}" build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)" printf '%s' "${REGISTRY_PASSWORD}" \ | docker login "${REGISTRY_HOST}" \ --username "${REGISTRY_USERNAME}" \ --password-stdin docker buildx build \ --platform linux/amd64 \ --file ./docker/Dockerfile \ --target runtime-web \ --build-arg INSTALL_DEV=false \ --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ --label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ --label "org.opencontainers.image.created=${build_time}" \ --tag "${web_image}:${immutable_tag}" \ --metadata-file web-metadata.json \ --push \ . docker buildx build \ --platform linux/amd64 \ --file ./docker/Dockerfile \ --target runtime-celery \ --build-arg INSTALL_DEV=false \ --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ --label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ --label "org.opencontainers.image.created=${build_time}" \ --tag "${celery_image}:${immutable_tag}" \ --metadata-file celery-metadata.json \ --push \ . metadata_digest() { sed -n \ 's/^[[:space:]]*"containerimage.digest":[[:space:]]*"\([^"]*\)".*/\1/p' \ "$1" \ | head -n 1 } web_digest="$(metadata_digest web-metadata.json)" celery_digest="$(metadata_digest celery-metadata.json)" digest_pattern='^sha256:[a-f0-9]{64}$' [[ "${web_digest}" =~ ${digest_pattern} ]] || { echo 'Buildx did not return a valid web image digest.' >&2 exit 1 } [[ "${celery_digest}" =~ ${digest_pattern} ]] || { echo 'Buildx did not return a valid Celery image digest.' >&2 exit 1 } release_run_id="${GITHUB_RUN_ID:-${GITHUB_SHA}}-${GITHUB_RUN_ATTEMPT:-1}" { printf 'web_ref=%s@%s\n' "${web_image}" "${web_digest}" printf 'celery_ref=%s@%s\n' "${celery_image}" "${celery_digest}" printf 'web_tag_ref=%s:%s\n' "${web_image}" "${immutable_tag}" printf 'celery_tag_ref=%s:%s\n' "${celery_image}" "${immutable_tag}" printf 'run_id=%s\n' "${release_run_id}" } >> "${GITHUB_OUTPUT}" - name: Remove Buildx builder if: always() run: | if [ -n "${BUILDER_NAME:-}" ]; then docker buildx rm "${BUILDER_NAME}" || true fi deploy_dev: name: Release dev runs-on: [backend-docker] timeout-minutes: 20 needs: [build] if: needs.build.result == 'success' && github.ref == 'refs/heads/dev' steps: - name: Checkout uses: actions/checkout@v4.2.2 with: persist-credentials: false - name: Release exact images env: RELEASE_HOST: ${{ secrets.DEPLOY_HOST }} RELEASE_KNOWN_HOSTS_B64: ${{ secrets.INTERNAL_KNOWN_HOSTS_B64 }} RELEASE_SERVICE_PATH: /opt/ecos-dev/release-service RELEASE_SSH_KEY_B64: ${{ secrets.INTERNAL_DEPLOY_SSH_KEY_B64 }} RELEASE_USER: ${{ secrets.DEPLOY_USER }} run: | ./scripts/ci/release-service.sh \ state-corp-backend \ "${{ needs.build.outputs.run_id }}" \ "${{ needs.build.outputs.web_ref }}" \ "${{ needs.build.outputs.celery_ref }}" - name: Smoke dev backend env: APP_BASE_URL: ${{ secrets.APP_BASE_URL }} run: ./scripts/ci/smoke.sh "${APP_BASE_URL%/}/health/ready/" deploy_main_internal: name: Refresh and release internal main runs-on: [backend-docker] timeout-minutes: 60 needs: [build] if: needs.build.result == 'success' && github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4.2.2 with: persist-credentials: false - name: Refresh cloned data and release exact images env: RELEASE_HOST: ${{ secrets.MAIN_DEPLOY_HOST }} RELEASE_KNOWN_HOSTS_B64: ${{ secrets.INTERNAL_KNOWN_HOSTS_B64 }} RELEASE_SERVICE_PATH: /opt/ecos-main/release-service RELEASE_SSH_KEY_B64: ${{ secrets.INTERNAL_DEPLOY_SSH_KEY_B64 }} RELEASE_USER: ${{ secrets.DEPLOY_USER }} run: | ./scripts/ci/release-service.sh \ state-corp-backend \ "${{ needs.build.outputs.run_id }}" \ "${{ needs.build.outputs.web_ref }}" \ "${{ needs.build.outputs.celery_ref }}" \ --refresh-data - name: Smoke internal main backend env: INTERNAL_MAIN_STATE_CORP_BACKEND_URL: ${{ secrets.INTERNAL_MAIN_STATE_CORP_BACKEND_URL }} run: ./scripts/ci/smoke.sh "${INTERNAL_MAIN_STATE_CORP_BACKEND_URL%/}/health/ready/" deploy_main_customer: name: Release customer main runs-on: [backend-docker] timeout-minutes: 30 needs: [build, deploy_main_internal] if: >- needs.build.result == 'success' && needs.deploy_main_internal.result == 'success' && github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4.2.2 with: persist-credentials: false - name: Release the same exact images without refresh env: RELEASE_HOST: 10.0.10.174 RELEASE_KNOWN_HOSTS_B64: ${{ secrets.CUSTOMER_KNOWN_HOSTS_B64 }} RELEASE_PROXY: ecos-proxy@10.10.0.121 RELEASE_SERVICE_PATH: /ecos/release-service RELEASE_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }} RELEASE_USER: ecos run: | ./scripts/ci/release-service.sh \ state-corp-backend \ "${{ needs.build.outputs.run_id }}" \ "${{ needs.build.outputs.web_ref }}" \ "${{ needs.build.outputs.celery_ref }}"