All checks were successful
CI/CD Pipeline / Manual Action Help (push) Has been skipped
CI/CD Pipeline / Start Dev Containers in Dokploy (push) Has been skipped
CI/CD Pipeline / Cleanup Dev Database (push) Has been skipped
CI/CD Pipeline / Quality Gate (push) Successful in 1m47s
CI/CD Pipeline / Build and Push Images (push) Successful in 2m49s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 1s
621 lines
22 KiB
YAML
621 lines
22 KiB
YAML
name: CI/CD Pipeline
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- dev
|
|
- "feature/**"
|
|
- "codex/**"
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- dev
|
|
workflow_dispatch:
|
|
inputs:
|
|
manual_action:
|
|
description: "Manual action: noop, cleanup_dev_database, or dokploy_start"
|
|
required: true
|
|
default: "noop"
|
|
dokploy_target:
|
|
description: "Dokploy dev target: all, web, worker, or beat"
|
|
required: true
|
|
default: "all"
|
|
cleanup_confirm:
|
|
description: "Type CLEAN_DEV_DB to recreate/reset the dev database as UTF8"
|
|
required: false
|
|
default: ""
|
|
|
|
concurrency:
|
|
group: mostovik-backend-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
PYTHON_VERSION: "3.11"
|
|
REGISTRY_API_URL: "https://registry.dev.nii-ecos.ru/v2/"
|
|
REGISTRY_HOST: "registry.dev.nii-ecos.ru"
|
|
REGISTRY_NAMESPACE: "${{ github.repository_owner }}"
|
|
WEB_IMAGE: "mostovik-backend-web"
|
|
CELERY_IMAGE: "mostovik-backend-celery"
|
|
DOKPLOY_DEV_WEB_WEBHOOK_URL: "https://deploy.dev.nii-ecos.ru/api/deploy/_EjfuYBpzGJ18uPwBZ3iF"
|
|
DOKPLOY_DEV_WORKER_WEBHOOK_URL: "https://deploy.dev.nii-ecos.ru/api/deploy/hltL7K2HmG1a8EIzr-mVA"
|
|
DOKPLOY_DEV_BEAT_WEBHOOK_URL: "https://deploy.dev.nii-ecos.ru/api/deploy/RkdykbqU6faErrZBAN9Rv"
|
|
UV_VERSION: "0.7.2"
|
|
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
|
|
|
jobs:
|
|
manual_action_noop:
|
|
name: Manual Action Help
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 1
|
|
if: |
|
|
github.event_name == 'workflow_dispatch' &&
|
|
github.ref == 'refs/heads/dev' &&
|
|
(github.event.inputs.manual_action == '' || github.event.inputs.manual_action == 'noop')
|
|
|
|
steps:
|
|
- name: Show manual action usage
|
|
run: |
|
|
set -euo pipefail
|
|
{
|
|
echo "No manual action selected."
|
|
echo "For dev DB cleanup run with:"
|
|
echo "- manual_action=cleanup_dev_database"
|
|
echo "- cleanup_confirm=CLEAN_DEV_DB"
|
|
echo "For Dokploy start run with manual_action=dokploy_start and dokploy_target=all|web|worker|beat."
|
|
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
|
|
quality:
|
|
name: Quality Gate
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
if: ${{ github.event_name != 'workflow_dispatch' }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
run: |
|
|
set -euo pipefail
|
|
REPO_URL=$(echo "${GITHUB_SERVER_URL}" | sed "s|://|://oauth2:${{ gitea.token }}@|")
|
|
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
|
git clone --depth=1 --branch="${BRANCH}" "${REPO_URL}/${GITHUB_REPOSITORY}.git" .
|
|
git checkout "${GITHUB_SHA}"
|
|
|
|
- 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: Create virtual environment and install dependencies
|
|
run: |
|
|
set -euo pipefail
|
|
. ./.ci-python-env
|
|
"${PYTHON_BIN}" -m venv .venv
|
|
. .venv/bin/activate
|
|
python -m pip install "uv==${UV_VERSION}"
|
|
uv sync \
|
|
--dev \
|
|
--frozen \
|
|
--active \
|
|
--python "${PYTHON_BIN}" \
|
|
--no-managed-python \
|
|
--no-python-downloads
|
|
|
|
- name: Run Ruff linting
|
|
if: ${{ !contains(github.event.head_commit.message, '#no_lint') }}
|
|
run: |
|
|
set -euo pipefail
|
|
. .venv/bin/activate
|
|
ruff check src
|
|
|
|
- name: Run Ruff formatting check
|
|
if: ${{ !contains(github.event.head_commit.message, '#no_lint') }}
|
|
run: |
|
|
set -euo pipefail
|
|
. .venv/bin/activate
|
|
ruff format src --check
|
|
|
|
- name: Run regular pytest suite
|
|
if: ${{ !contains(github.event.head_commit.message, '#no_test') }}
|
|
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 --ignore=tests/test_api_inventory_e2e.py -q
|
|
|
|
- name: Run API inventory pytest suite
|
|
if: ${{ !contains(github.event.head_commit.message, '#no_test') }}
|
|
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/test_api_inventory_e2e.py -q
|
|
|
|
build_push:
|
|
name: Build and Push Images
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
needs: [quality]
|
|
if: |
|
|
github.event_name == 'push' &&
|
|
(github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') &&
|
|
needs.quality.result == 'success' &&
|
|
!contains(github.event.head_commit.message, '#no_image')
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
run: |
|
|
set -euo pipefail
|
|
REPO_URL=$(echo "${GITHUB_SERVER_URL}" | sed "s|://|://oauth2:${{ gitea.token }}@|")
|
|
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
|
git clone --depth=1 --branch="${BRANCH}" "${REPO_URL}/${GITHUB_REPOSITORY}.git" .
|
|
git checkout "${GITHUB_SHA}"
|
|
|
|
- name: Build and push branch images
|
|
env:
|
|
GITEA_TOKEN: ${{ gitea.token }}
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-branch}}"
|
|
BRANCH_TAG=$(printf '%s' "${BRANCH}" \
|
|
| tr '[:upper:]' '[:lower:]' \
|
|
| sed -E 's#[/[:space:]]+#-#g; s#[^a-z0-9_.-]+#-#g; s#^-+##; s#-+$##')
|
|
BRANCH_TAG="${BRANCH_TAG:-branch}"
|
|
BRANCH_TAG=$(printf '%.120s' "${BRANCH_TAG}")
|
|
SHA_SHORT=$(printf '%s' "${GITHUB_SHA}" | cut -c1-7)
|
|
|
|
REGISTRY_PATH="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}"
|
|
WEB_REF="${REGISTRY_PATH}/${WEB_IMAGE}"
|
|
CELERY_REF="${REGISTRY_PATH}/${CELERY_IMAGE}"
|
|
REGISTRY_USER="${REGISTRY_USER:-${GITHUB_ACTOR}}"
|
|
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-${GITEA_TOKEN:-}}"
|
|
|
|
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY
|
|
export NO_PROXY="${NO_PROXY:-},${REGISTRY_HOST}"
|
|
export no_proxy="${no_proxy:-},${REGISTRY_HOST}"
|
|
|
|
if [ -z "${REGISTRY_PASSWORD}" ]; then
|
|
echo "REGISTRY_TOKEN secret is not set and gitea.token fallback is empty" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "${REGISTRY_PASSWORD}" \
|
|
| docker login "${REGISTRY_HOST}" \
|
|
-u "${REGISTRY_USER}" \
|
|
--password-stdin
|
|
|
|
WEB_TAGS=(
|
|
-t "${WEB_REF}:${BRANCH_TAG}"
|
|
-t "${WEB_REF}:${BRANCH_TAG}-${SHA_SHORT}"
|
|
)
|
|
CELERY_TAGS=(
|
|
-t "${CELERY_REF}:${BRANCH_TAG}"
|
|
-t "${CELERY_REF}:${BRANCH_TAG}-${SHA_SHORT}"
|
|
)
|
|
if [ "${GITHUB_REF_NAME}" = "main" ]; then
|
|
WEB_TAGS+=(-t "${WEB_REF}:latest")
|
|
CELERY_TAGS+=(-t "${CELERY_REF}:latest")
|
|
fi
|
|
|
|
docker build \
|
|
-f ./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}" \
|
|
"${WEB_TAGS[@]}" \
|
|
.
|
|
|
|
docker push "${WEB_REF}:${BRANCH_TAG}"
|
|
docker push "${WEB_REF}:${BRANCH_TAG}-${SHA_SHORT}"
|
|
if [ "${GITHUB_REF_NAME}" = "main" ]; then
|
|
docker push "${WEB_REF}:latest"
|
|
fi
|
|
|
|
docker build \
|
|
-f ./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}" \
|
|
"${CELERY_TAGS[@]}" \
|
|
.
|
|
|
|
docker push "${CELERY_REF}:${BRANCH_TAG}"
|
|
docker push "${CELERY_REF}:${BRANCH_TAG}-${SHA_SHORT}"
|
|
if [ "${GITHUB_REF_NAME}" = "main" ]; then
|
|
docker push "${CELERY_REF}:latest"
|
|
fi
|
|
|
|
{
|
|
echo "Registry API: ${REGISTRY_API_URL}"
|
|
echo "Pushed images:"
|
|
echo "- ${WEB_REF}:${BRANCH_TAG}"
|
|
echo "- ${WEB_REF}:${BRANCH_TAG}-${SHA_SHORT}"
|
|
echo "- ${CELERY_REF}:${BRANCH_TAG}"
|
|
echo "- ${CELERY_REF}:${BRANCH_TAG}-${SHA_SHORT}"
|
|
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
|
|
notify:
|
|
name: Internal Notify
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 1
|
|
needs: [quality, build_push]
|
|
if: ${{ always() && github.event_name != 'workflow_dispatch' }}
|
|
|
|
steps:
|
|
- name: Send CI status webhook
|
|
continue-on-error: true
|
|
env:
|
|
CI_NOTIFY_WEBHOOK_URL: ${{ secrets.CI_NOTIFY_WEBHOOK_URL }}
|
|
CI_NOTIFY_TOKEN: ${{ secrets.CI_NOTIFY_TOKEN }}
|
|
QUALITY_RESULT: ${{ needs.quality.result }}
|
|
BUILD_PUSH_RESULT: ${{ needs.build_push.result }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${CI_NOTIFY_WEBHOOK_URL:-}" ]; then
|
|
echo "CI_NOTIFY_WEBHOOK_URL is not set; skip internal notification"
|
|
exit 0
|
|
fi
|
|
|
|
STATUS="success"
|
|
if [ "${QUALITY_RESULT}" != "success" ]; then
|
|
STATUS="${QUALITY_RESULT}"
|
|
elif [ "${BUILD_PUSH_RESULT}" = "failure" ] || [ "${BUILD_PUSH_RESULT}" = "cancelled" ]; then
|
|
STATUS="${BUILD_PUSH_RESULT}"
|
|
fi
|
|
export STATUS
|
|
|
|
PAYLOAD=$(python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
payload = {
|
|
"project": os.environ.get("GITHUB_REPOSITORY"),
|
|
"workflow": os.environ.get("GITHUB_WORKFLOW"),
|
|
"status": os.environ.get("STATUS"),
|
|
"branch": os.environ.get("GITHUB_HEAD_REF") or os.environ.get("GITHUB_REF_NAME"),
|
|
"sha": os.environ.get("GITHUB_SHA"),
|
|
"actor": os.environ.get("GITHUB_ACTOR"),
|
|
"server_url": os.environ.get("GITHUB_SERVER_URL"),
|
|
"run_id": os.environ.get("GITHUB_RUN_ID"),
|
|
"results": {
|
|
"quality": os.environ.get("QUALITY_RESULT"),
|
|
"build_push": os.environ.get("BUILD_PUSH_RESULT"),
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=True, separators=(",", ":")))
|
|
PY
|
|
)
|
|
|
|
AUTH_HEADER=()
|
|
if [ -n "${CI_NOTIFY_TOKEN:-}" ]; then
|
|
AUTH_HEADER=(-H "Authorization: Bearer ${CI_NOTIFY_TOKEN}")
|
|
fi
|
|
|
|
curl -fsS \
|
|
--connect-timeout 3 \
|
|
--max-time 8 \
|
|
--retry 1 \
|
|
-H "Content-Type: application/json" \
|
|
"${AUTH_HEADER[@]}" \
|
|
--data "${PAYLOAD}" \
|
|
"${CI_NOTIFY_WEBHOOK_URL}"
|
|
|
|
deploy_dev:
|
|
name: Deploy Dev in Dokploy
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
needs: [build_push]
|
|
if: |
|
|
github.event_name == 'push' &&
|
|
github.ref == 'refs/heads/dev' &&
|
|
needs.build_push.result == 'success' &&
|
|
!contains(github.event.head_commit.message, '#no_deploy')
|
|
|
|
steps:
|
|
- name: Trigger dev Dokploy webhooks
|
|
env:
|
|
DOKPLOY_API_TOKEN: ${{ secrets.DOKPLOY_API_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
call_webhook() {
|
|
service_name="$1"
|
|
webhook_url="$2"
|
|
target="$3"
|
|
|
|
AUTH_HEADER=()
|
|
if [ -n "${DOKPLOY_API_TOKEN:-}" ]; then
|
|
AUTH_HEADER=(-H "Authorization: Bearer ${DOKPLOY_API_TOKEN}")
|
|
fi
|
|
|
|
PAYLOAD=$(CURRENT_DOKPLOY_TARGET="${target}" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
celery_image = f"{os.environ['REGISTRY_HOST']}/{os.environ['REGISTRY_NAMESPACE']}/{os.environ['CELERY_IMAGE']}:dev"
|
|
payload = {
|
|
"project": os.environ.get("GITHUB_REPOSITORY"),
|
|
"branch": os.environ.get("GITHUB_REF_NAME"),
|
|
"sha": os.environ.get("GITHUB_SHA"),
|
|
"actor": os.environ.get("GITHUB_ACTOR"),
|
|
"target": os.environ.get("CURRENT_DOKPLOY_TARGET"),
|
|
"images": {
|
|
"web": f"{os.environ['REGISTRY_HOST']}/{os.environ['REGISTRY_NAMESPACE']}/{os.environ['WEB_IMAGE']}:dev",
|
|
"worker": celery_image,
|
|
"beat": celery_image,
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=True, separators=(",", ":")))
|
|
PY
|
|
)
|
|
|
|
echo "Trigger Dokploy for ${service_name}"
|
|
curl -fsS \
|
|
--connect-timeout 5 \
|
|
--max-time 30 \
|
|
--retry 2 \
|
|
--retry-delay 2 \
|
|
-X POST \
|
|
-H "Content-Type: application/json" \
|
|
"${AUTH_HEADER[@]}" \
|
|
--data "${PAYLOAD}" \
|
|
"${webhook_url}"
|
|
}
|
|
|
|
call_webhook "dev web" "${DOKPLOY_DEV_WEB_WEBHOOK_URL}" "web"
|
|
call_webhook "dev worker" "${DOKPLOY_DEV_WORKER_WEBHOOK_URL}" "worker"
|
|
call_webhook "dev beat" "${DOKPLOY_DEV_BEAT_WEBHOOK_URL}" "beat"
|
|
|
|
{
|
|
echo "Dokploy dev deploy triggered."
|
|
echo "Web image: ${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${WEB_IMAGE}:dev"
|
|
echo "Worker image: ${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${CELERY_IMAGE}:dev"
|
|
echo "Beat image: ${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${CELERY_IMAGE}:dev"
|
|
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
|
|
dokploy_dev_start:
|
|
name: Start Dev Containers in Dokploy
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
if: |
|
|
github.event_name == 'workflow_dispatch' &&
|
|
github.ref == 'refs/heads/dev' &&
|
|
github.event.inputs.manual_action == 'dokploy_start'
|
|
|
|
steps:
|
|
- name: Trigger Dokploy webhooks
|
|
env:
|
|
DOKPLOY_TARGET: ${{ github.event.inputs.dokploy_target }}
|
|
DOKPLOY_API_TOKEN: ${{ secrets.DOKPLOY_API_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
TARGET="${DOKPLOY_TARGET:-all}"
|
|
case "${TARGET}" in
|
|
all|web|worker|celery|beat) ;;
|
|
*)
|
|
echo "dokploy_target must be one of: all, web, worker, beat" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
call_webhook() {
|
|
service_name="$1"
|
|
webhook_url="$2"
|
|
target="$3"
|
|
|
|
if [ -z "${webhook_url}" ]; then
|
|
echo "Dokploy webhook for ${service_name} is not configured" >&2
|
|
return 1
|
|
fi
|
|
|
|
AUTH_HEADER=()
|
|
if [ -n "${DOKPLOY_API_TOKEN:-}" ]; then
|
|
AUTH_HEADER=(-H "Authorization: Bearer ${DOKPLOY_API_TOKEN}")
|
|
fi
|
|
|
|
PAYLOAD=$(CURRENT_DOKPLOY_TARGET="${target}" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
celery_image = f"{os.environ['REGISTRY_HOST']}/{os.environ['REGISTRY_NAMESPACE']}/{os.environ['CELERY_IMAGE']}:dev"
|
|
payload = {
|
|
"project": os.environ.get("GITHUB_REPOSITORY"),
|
|
"branch": os.environ.get("GITHUB_REF_NAME"),
|
|
"sha": os.environ.get("GITHUB_SHA"),
|
|
"actor": os.environ.get("GITHUB_ACTOR"),
|
|
"target": os.environ.get("CURRENT_DOKPLOY_TARGET"),
|
|
"images": {
|
|
"web": f"{os.environ['REGISTRY_HOST']}/{os.environ['REGISTRY_NAMESPACE']}/{os.environ['WEB_IMAGE']}:dev",
|
|
"worker": celery_image,
|
|
"beat": celery_image,
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=True, separators=(",", ":")))
|
|
PY
|
|
)
|
|
|
|
echo "Trigger Dokploy for ${service_name}"
|
|
curl -fsS \
|
|
--connect-timeout 5 \
|
|
--max-time 30 \
|
|
--retry 2 \
|
|
--retry-delay 2 \
|
|
-X POST \
|
|
-H "Content-Type: application/json" \
|
|
"${AUTH_HEADER[@]}" \
|
|
--data "${PAYLOAD}" \
|
|
"${webhook_url}"
|
|
}
|
|
|
|
triggered=0
|
|
|
|
if [ "${TARGET}" = "all" ] || [ "${TARGET}" = "web" ]; then
|
|
call_webhook "dev web" "${DOKPLOY_DEV_WEB_WEBHOOK_URL}" "web"
|
|
triggered=1
|
|
fi
|
|
|
|
if [ "${TARGET}" = "all" ] || [ "${TARGET}" = "worker" ] || [ "${TARGET}" = "celery" ]; then
|
|
call_webhook "dev worker" "${DOKPLOY_DEV_WORKER_WEBHOOK_URL}" "worker"
|
|
triggered=1
|
|
fi
|
|
|
|
if [ "${TARGET}" = "all" ] || [ "${TARGET}" = "beat" ]; then
|
|
call_webhook "dev beat" "${DOKPLOY_DEV_BEAT_WEBHOOK_URL}" "beat"
|
|
triggered=1
|
|
fi
|
|
|
|
if [ "${triggered}" -ne 1 ]; then
|
|
echo "No Dokploy webhook was triggered" >&2
|
|
exit 1
|
|
fi
|
|
|
|
{
|
|
echo "Dokploy dev trigger completed."
|
|
echo "Target: ${TARGET}"
|
|
echo "Registry API: ${REGISTRY_API_URL}"
|
|
echo "Web image: ${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${WEB_IMAGE}:dev"
|
|
echo "Worker image: ${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${CELERY_IMAGE}:dev"
|
|
echo "Beat image: ${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${CELERY_IMAGE}:dev"
|
|
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
|
|
cleanup_dev_database:
|
|
name: Cleanup Dev Database
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
if: |
|
|
github.event_name == 'workflow_dispatch' &&
|
|
github.ref == 'refs/heads/dev' &&
|
|
github.event.inputs.manual_action == 'cleanup_dev_database'
|
|
env:
|
|
POSTGRES_HOST: "10.10.0.114"
|
|
POSTGRES_PORT: "5432"
|
|
POSTGRES_DB: "mostovik"
|
|
POSTGRES_USER: "postgres"
|
|
POSTGRES_PASSWORD: "postgres"
|
|
|
|
steps:
|
|
- name: Validate confirmation
|
|
env:
|
|
CONFIRM: ${{ github.event.inputs.cleanup_confirm }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "${CONFIRM}" != "CLEAN_DEV_DB" ]; then
|
|
echo "Manual confirmation must be exactly CLEAN_DEV_DB" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Install PostgreSQL client
|
|
run: |
|
|
set -euo pipefail
|
|
APT_RUNNER=()
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
APT_RUNNER=(sudo)
|
|
fi
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
"${APT_RUNNER[@]}" apt-get update
|
|
"${APT_RUNNER[@]}" apt-get install -y postgresql-client
|
|
|
|
- name: Recreate UTF8 database or reset public schema
|
|
run: |
|
|
set -euo pipefail
|
|
export PGPASSWORD="${POSTGRES_PASSWORD}"
|
|
|
|
terminate_connections() {
|
|
psql \
|
|
--set ON_ERROR_STOP=1 \
|
|
--host="${POSTGRES_HOST}" \
|
|
--port="${POSTGRES_PORT}" \
|
|
--username="${POSTGRES_USER}" \
|
|
--dbname=postgres \
|
|
--set=dbname="${POSTGRES_DB}" \
|
|
<<'SQL'
|
|
SELECT pg_terminate_backend(pid)
|
|
FROM pg_stat_activity
|
|
WHERE datname = :'dbname'
|
|
AND pid <> pg_backend_pid();
|
|
SQL
|
|
}
|
|
|
|
ENCODING=$(psql \
|
|
--set ON_ERROR_STOP=1 \
|
|
--host="${POSTGRES_HOST}" \
|
|
--port="${POSTGRES_PORT}" \
|
|
--username="${POSTGRES_USER}" \
|
|
--dbname=postgres \
|
|
--tuples-only \
|
|
--no-align \
|
|
--set=dbname="${POSTGRES_DB}" \
|
|
--command="SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = :'dbname';")
|
|
|
|
if [ "${ENCODING:-}" != "UTF8" ]; then
|
|
echo "Database ${POSTGRES_DB} encoding is ${ENCODING:-missing}; recreating as UTF8"
|
|
terminate_connections
|
|
psql \
|
|
--set ON_ERROR_STOP=1 \
|
|
--host="${POSTGRES_HOST}" \
|
|
--port="${POSTGRES_PORT}" \
|
|
--username="${POSTGRES_USER}" \
|
|
--dbname=postgres \
|
|
--set=dbname="${POSTGRES_DB}" \
|
|
--set=dbuser="${POSTGRES_USER}" \
|
|
<<'SQL'
|
|
DROP DATABASE IF EXISTS :"dbname";
|
|
CREATE DATABASE :"dbname" WITH OWNER :"dbuser" TEMPLATE template0 ENCODING 'UTF8';
|
|
SQL
|
|
else
|
|
echo "Database ${POSTGRES_DB} is already UTF8; resetting public schema"
|
|
terminate_connections
|
|
psql \
|
|
--set ON_ERROR_STOP=1 \
|
|
--host="${POSTGRES_HOST}" \
|
|
--port="${POSTGRES_PORT}" \
|
|
--username="${POSTGRES_USER}" \
|
|
--dbname="${POSTGRES_DB}" \
|
|
--set=dbuser="${POSTGRES_USER}" \
|
|
<<'SQL'
|
|
DROP SCHEMA IF EXISTS public CASCADE;
|
|
CREATE SCHEMA public AUTHORIZATION :"dbuser";
|
|
GRANT ALL ON SCHEMA public TO :"dbuser";
|
|
GRANT ALL ON SCHEMA public TO public;
|
|
SQL
|
|
fi
|
|
|
|
psql \
|
|
--set ON_ERROR_STOP=1 \
|
|
--host="${POSTGRES_HOST}" \
|
|
--port="${POSTGRES_PORT}" \
|
|
--username="${POSTGRES_USER}" \
|
|
--dbname=postgres \
|
|
--tuples-only \
|
|
--no-align \
|
|
--set=dbname="${POSTGRES_DB}" \
|
|
--command="SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = :'dbname';" \
|
|
| tee /tmp/mostovik-db-encoding
|
|
|
|
if [ "$(cat /tmp/mostovik-db-encoding)" != "UTF8" ]; then
|
|
echo "Database ${POSTGRES_DB} is not UTF8 after cleanup" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Summary
|
|
run: |
|
|
set -euo pipefail
|
|
{
|
|
echo "Dev database cleanup completed."
|
|
echo "Database: ${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
|
echo "Encoding: UTF8"
|
|
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|