Repository navigation
Merge pull request #754 from PROCOLLAB-github/fix/dev-member-name-search #471
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Dev | |
| on: | |
| push: | |
| branches: | |
| - dev | |
| workflow_dispatch: | |
| jobs: | |
| deploy: | |
| name: Deploy | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Deploy to server | |
| uses: garygrossgarten/github-action-ssh@release | |
| with: | |
| host: ${{ secrets.DEV_SERVER_HOST }} | |
| username: ${{ secrets.DEV_SERVER_USER }} | |
| password: ${{ secrets.DEV_SERVER_PASSWORD }} | |
| command: | | |
| set -eu | |
| cd /root/api | |
| git fetch --all --tags --prune | |
| git branch -r --contains "${{ github.sha }}" | grep -q 'origin/dev' | |
| git checkout --detach "${{ github.sha }}" | |
| git reset --hard "${{ github.sha }}" | |
| test "$(git rev-parse HEAD)" = "${{ github.sha }}" | |
| if [ -n "$(git status --porcelain --untracked-files=no)" ]; then | |
| echo "Tracked working tree is dirty after reset" >&2 | |
| git status --short --untracked-files=no >&2 | |
| exit 1 | |
| fi | |
| expected_serializer_blob="$(git rev-parse HEAD:vacancy/serializers.py)" | |
| actual_serializer_blob="$(git hash-object vacancy/serializers.py)" | |
| if [ "$actual_serializer_blob" != "$expected_serializer_blob" ]; then | |
| echo "Server serializers.py does not match HEAD blob" >&2 | |
| exit 1 | |
| fi | |
| echo "Deploy source HEAD: $(git rev-parse HEAD)" | |
| echo "vacancy/serializers.py blob: ${actual_serializer_blob}" | |
| export IMAGE_TAG="${{ github.sha }}" | |
| SOURCE_IMAGE="procollab-dev-api:${IMAGE_TAG}" | |
| export DEPLOY_IMAGE="$SOURCE_IMAGE" | |
| rm -f .env | |
| touch .env | |
| echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env | |
| echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env | |
| echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env | |
| echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env | |
| echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env | |
| echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env | |
| echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env | |
| echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env | |
| echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env | |
| echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env | |
| echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env | |
| echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env | |
| chmod 600 .env | |
| docker compose -f docker-compose.dev-ci.yml config >/dev/null | |
| docker compose -f docker-compose.dev-ci.yml build web | |
| BUILT_IMAGE_ID="$( | |
| docker image inspect \ | |
| -f '{{.Id}}' \ | |
| "$SOURCE_IMAGE" | |
| )" | |
| echo "Built image: ${SOURCE_IMAGE}" | |
| echo "Built image ID: ${BUILT_IMAGE_ID}" | |
| case "$BUILT_IMAGE_ID" in | |
| sha256:*) ;; | |
| *) | |
| echo "Invalid built image ID: ${BUILT_IMAGE_ID}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| BUILT_IMAGE_SHORT="$( | |
| printf '%s' "${BUILT_IMAGE_ID#sha256:}" | | |
| cut -c1-12 | |
| )" | |
| RUNTIME_IMAGE_TAG="${IMAGE_TAG}-${BUILT_IMAGE_SHORT}" | |
| RUNTIME_IMAGE="procollab-dev-api:${RUNTIME_IMAGE_TAG}" | |
| docker image rm "$RUNTIME_IMAGE" >/dev/null 2>&1 || true | |
| docker tag "$BUILT_IMAGE_ID" "$RUNTIME_IMAGE" | |
| RUNTIME_IMAGE_ID="$( | |
| docker image inspect -f '{{.Id}}' "$RUNTIME_IMAGE" | |
| )" | |
| test "$RUNTIME_IMAGE_ID" = "$BUILT_IMAGE_ID" | |
| export DEPLOY_IMAGE="$RUNTIME_IMAGE" | |
| echo "Pinned runtime image: ${RUNTIME_IMAGE}" | |
| echo "Pinned runtime image ID: ${RUNTIME_IMAGE_ID}" | |
| docker compose -f docker-compose.dev-ci.yml config >/dev/null | |
| docker compose -f docker-compose.dev-ci.yml config --format json | | |
| docker run --rm -i \ | |
| --entrypoint python \ | |
| "$RUNTIME_IMAGE" \ | |
| -c ' | |
| import json | |
| import sys | |
| expected = sys.argv[1] | |
| services = json.load(sys.stdin)["services"] | |
| for service_name in ("web", "celerys"): | |
| actual = services[service_name]["image"] | |
| print(f"Compose {service_name} image: {actual}") | |
| if actual != expected: | |
| raise SystemExit( | |
| f"Compose {service_name} does not use {expected}" | |
| ) | |
| ' "$RUNTIME_IMAGE" | |
| BUILT_SERIALIZER_SHA="$( | |
| docker run --rm \ | |
| --entrypoint sha256sum \ | |
| "$RUNTIME_IMAGE" \ | |
| /procollab/vacancy/serializers.py | | |
| awk '{print $1}' | |
| )" | |
| test -n "$BUILT_SERIALIZER_SHA" | |
| echo "Built vacancy/serializers.py SHA-256: ${BUILT_SERIALIZER_SHA}" | |
| docker compose -f docker-compose.dev-ci.yml run --rm -T web python - <<'PY' | |
| import os | |
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "procollab.settings") | |
| import django | |
| django.setup() | |
| from vacancy.serializers import ( | |
| ProjectVacancyCreateListSerializer, | |
| VacancyDetailSerializer, | |
| ) | |
| create_fields = ProjectVacancyCreateListSerializer.Meta.fields | |
| detail_fields = VacancyDetailSerializer.Meta.fields | |
| print("Compose run create serializer fields:", create_fields) | |
| print("Compose run detail serializer fields:", detail_fields) | |
| if "city" not in create_fields: | |
| raise SystemExit("Compose run create serializer uses stale image") | |
| if "city" not in detail_fields: | |
| raise SystemExit("Compose run detail serializer uses stale image") | |
| PY | |
| docker compose -f docker-compose.dev-ci.yml run --rm web \ | |
| python manage.py migrate | |
| docker compose -f docker-compose.dev-ci.yml run --rm web \ | |
| python manage.py showmigrations vacancy | |
| docker compose -f docker-compose.dev-ci.yml rm -sf web celerys || true | |
| docker compose -f docker-compose.dev-ci.yml up \ | |
| -d \ | |
| --no-build \ | |
| --force-recreate | |
| for service in web celerys; do | |
| container="$(docker compose -f docker-compose.dev-ci.yml ps -q "$service")" | |
| if [ -z "$container" ]; then | |
| echo "Service ${service} has no running container" >&2 | |
| docker compose -f docker-compose.dev-ci.yml ps >&2 || true | |
| exit 1 | |
| fi | |
| configured_image="$(docker inspect -f '{{.Config.Image}}' "$container")" | |
| actual_image_id="$(docker inspect -f '{{.Image}}' "$container")" | |
| echo "Service ${service}: container=${container} reference=${configured_image} image_id=${actual_image_id}" | |
| if [ "$actual_image_id" != "$BUILT_IMAGE_ID" ]; then | |
| echo "Service ${service} uses wrong image ID" >&2 | |
| echo "Expected: ${BUILT_IMAGE_ID}" >&2 | |
| echo "Actual: ${actual_image_id}" >&2 | |
| echo "Reference: ${configured_image}" >&2 | |
| exit 1 | |
| fi | |
| done | |
| RUNNING_SERIALIZER_SHA="$( | |
| docker compose -f docker-compose.dev-ci.yml exec -T web \ | |
| sha256sum /procollab/vacancy/serializers.py | | |
| awk '{print $1}' | |
| )" | |
| echo "Running vacancy/serializers.py SHA-256: ${RUNNING_SERIALIZER_SHA}" | |
| if [ "$RUNNING_SERIALIZER_SHA" != "$BUILT_SERIALIZER_SHA" ]; then | |
| echo "Running serializer source does not match built image" >&2 | |
| echo "Expected: ${BUILT_SERIALIZER_SHA}" >&2 | |
| echo "Actual: ${RUNNING_SERIALIZER_SHA}" >&2 | |
| exit 1 | |
| fi | |
| docker compose -f docker-compose.dev-ci.yml exec -T web python - <<'PY' | |
| import os | |
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "procollab.settings") | |
| import django | |
| django.setup() | |
| from django.db import connection | |
| from django.db.migrations.recorder import MigrationRecorder | |
| applied = { | |
| (migration.app, migration.name) | |
| for migration in MigrationRecorder(connection).migration_qs | |
| } | |
| if ("vacancy", "0010_vacancy_city") not in applied: | |
| raise SystemExit("vacancy.0010_vacancy_city is not applied") | |
| print("vacancy.0010_vacancy_city applied") | |
| PY | |
| docker compose -f docker-compose.dev-ci.yml exec -T web python - <<'PY' | |
| import os | |
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "procollab.settings") | |
| import django | |
| django.setup() | |
| from vacancy.serializers import ( | |
| ProjectVacancyCreateListSerializer, | |
| VacancyDetailSerializer, | |
| ) | |
| create_fields = set(ProjectVacancyCreateListSerializer().fields.keys()) | |
| detail_fields = set(VacancyDetailSerializer().fields.keys()) | |
| print("Runtime create serializer fields:", sorted(create_fields)) | |
| print("Runtime detail serializer fields:", sorted(detail_fields)) | |
| if "city" not in create_fields: | |
| raise SystemExit("Runtime create serializer does not contain city") | |
| if "city" not in detail_fields: | |
| raise SystemExit("Runtime detail serializer does not contain city") | |
| print("Runtime serializers contain city") | |
| PY | |
| install -d /etc/nginx/procollab/includes && | |
| install -m 644 deploy/nginx/host/includes/proxy_app.inc /etc/nginx/procollab/includes/proxy_app.inc && | |
| install -m 644 deploy/nginx/host/dev/dev.procollab.ru /etc/nginx/sites-available/dev.procollab.ru && | |
| ln -sfn /etc/nginx/sites-available/dev.procollab.ru /etc/nginx/sites-enabled/dev.procollab.ru && | |
| if [ "$(id -u)" -eq 0 ]; then | |
| nginx -t && | |
| systemctl reload nginx | |
| else | |
| sudo nginx -t && | |
| sudo systemctl reload nginx | |
| fi && | |
| for attempt in $(seq 1 24); do | |
| admin_status="$(curl -s -o /dev/null -w '%{http_code}' https://dev.procollab.ru/admin/login/ || true)" && | |
| if [ "$admin_status" = "200" ]; then | |
| echo "Smoke check passed on attempt ${attempt}" && | |
| break | |
| fi | |
| sleep 5 | |
| done && | |
| if [ "$admin_status" != "200" ]; then | |
| echo "Smoke check failed: /admin/login/=${admin_status}" >&2 && | |
| exit 1 | |
| fi && | |
| docker compose -f docker-compose.dev-ci.yml ps | |
| celery_status="" | |
| celery_ping="" | |
| celery_container="" | |
| for attempt in $(seq 1 12); do | |
| celery_container="$(docker compose -f docker-compose.dev-ci.yml ps -q celerys 2>/dev/null || true)" | |
| if [ -n "$celery_container" ]; then | |
| celery_status="$(docker inspect -f '{{.State.Status}}' "$celery_container" 2>/dev/null || true)" | |
| else | |
| celery_status="missing" | |
| fi | |
| echo "Celery check attempt ${attempt}: container=${celery_container:-missing} status=${celery_status}" | |
| if [ "$celery_status" = "running" ]; then | |
| celery_ping="$(docker compose -f docker-compose.dev-ci.yml exec -T celerys sh -lc 'celery -A procollab inspect ping --timeout=15' 2>&1 || true)" | |
| printf '%s\n' "$celery_ping" | |
| if printf '%s\n' "$celery_ping" | grep -q 'pong'; then | |
| echo "Celery check passed on attempt ${attempt}" | |
| break | |
| fi | |
| fi | |
| sleep 5 | |
| done | |
| if [ "$celery_status" != "running" ] || ! printf '%s\n' "$celery_ping" | grep -q 'pong'; then | |
| echo "Celery check failed: status=${celery_status}" >&2 | |
| docker compose -f docker-compose.dev-ci.yml ps >&2 || true | |
| docker compose -f docker-compose.dev-ci.yml logs --tail=200 celerys >&2 || true | |
| docker compose -f docker-compose.dev-ci.yml logs --tail=100 redis >&2 || true | |
| docker compose -f docker-compose.dev-ci.yml logs --tail=100 web >&2 || true | |
| exit 1 | |
| fi |