diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 2972ad5..cb16219 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -22,7 +22,7 @@ on: required: true default: "all" cleanup_confirm: - description: "Type CLEAN_DEV_DB to recreate/reset the dev database as UTF8" + description: "Type CLEAN_DEV_DB to drop and recreate the dev database as UTF8" required: false default: "" @@ -62,6 +62,7 @@ jobs: echo "For dev DB cleanup run with:" echo "- manual_action=cleanup_dev_database" echo "- cleanup_confirm=CLEAN_DEV_DB" + echo "This drops and recreates the dev database, then triggers Dokploy web/worker/beat." echo "For Dokploy start run with manual_action=dokploy_start and dokploy_target=all|web|worker|beat." } >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}" @@ -343,11 +344,40 @@ jobs: import json import os + repository = os.environ.get("GITHUB_REPOSITORY", "") + repository_name = repository.rsplit("/", 1)[-1] + branch = os.environ.get("GITHUB_REF_NAME") or "dev" + sha = os.environ.get("GITHUB_SHA") or "" + server_url = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") + repository_url = f"{server_url}/{repository}" if server_url and repository else "" 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"), + "ref": f"refs/heads/{branch}", + "after": sha, + "checkout_sha": sha, + "repository": { + "name": repository_name, + "full_name": repository, + "html_url": repository_url, + "clone_url": f"{repository_url}.git" if repository_url else "", + }, + "sender": {"login": os.environ.get("GITHUB_ACTOR")}, + "pusher": {"name": os.environ.get("GITHUB_ACTOR")}, + "head_commit": { + "id": sha, + "message": f"CI deploy {os.environ.get('CURRENT_DOKPLOY_TARGET')}", + "url": f"{repository_url}/commit/{sha}" if repository_url and sha else "", + }, + "commits": [ + { + "id": sha, + "message": f"CI deploy {os.environ.get('CURRENT_DOKPLOY_TARGET')}", + "url": f"{repository_url}/commit/{sha}" if repository_url and sha else "", + } + ], + "project": repository, + "branch": branch, + "sha": sha, "actor": os.environ.get("GITHUB_ACTOR"), "target": os.environ.get("CURRENT_DOKPLOY_TARGET"), "images": { @@ -361,16 +391,24 @@ jobs: ) echo "Trigger Dokploy for ${service_name}" - curl -fsS \ + RESPONSE=$(curl -fsS \ --connect-timeout 5 \ --max-time 30 \ --retry 2 \ --retry-delay 2 \ -X POST \ -H "Content-Type: application/json" \ + -H "X-Gitea-Event: push" \ + -H "X-Gogs-Event: push" \ + -H "X-GitHub-Event: push" \ "${AUTH_HEADER[@]}" \ --data "${PAYLOAD}" \ - "${webhook_url}" + "${webhook_url}") + printf '%s\n' "${RESPONSE}" + if printf '%s' "${RESPONSE}" | grep -qi "Branch Not Match"; then + echo "Dokploy rejected ${service_name}: branch did not match" >&2 + exit 1 + fi } call_webhook "dev web" "${DOKPLOY_DEV_WEB_WEBHOOK_URL}" "web" @@ -429,11 +467,40 @@ jobs: import json import os + repository = os.environ.get("GITHUB_REPOSITORY", "") + repository_name = repository.rsplit("/", 1)[-1] + branch = os.environ.get("GITHUB_REF_NAME") or "dev" + sha = os.environ.get("GITHUB_SHA") or "" + server_url = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") + repository_url = f"{server_url}/{repository}" if server_url and repository else "" 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"), + "ref": f"refs/heads/{branch}", + "after": sha, + "checkout_sha": sha, + "repository": { + "name": repository_name, + "full_name": repository, + "html_url": repository_url, + "clone_url": f"{repository_url}.git" if repository_url else "", + }, + "sender": {"login": os.environ.get("GITHUB_ACTOR")}, + "pusher": {"name": os.environ.get("GITHUB_ACTOR")}, + "head_commit": { + "id": sha, + "message": f"CI deploy {os.environ.get('CURRENT_DOKPLOY_TARGET')}", + "url": f"{repository_url}/commit/{sha}" if repository_url and sha else "", + }, + "commits": [ + { + "id": sha, + "message": f"CI deploy {os.environ.get('CURRENT_DOKPLOY_TARGET')}", + "url": f"{repository_url}/commit/{sha}" if repository_url and sha else "", + } + ], + "project": repository, + "branch": branch, + "sha": sha, "actor": os.environ.get("GITHUB_ACTOR"), "target": os.environ.get("CURRENT_DOKPLOY_TARGET"), "images": { @@ -447,16 +514,24 @@ jobs: ) echo "Trigger Dokploy for ${service_name}" - curl -fsS \ + RESPONSE=$(curl -fsS \ --connect-timeout 5 \ --max-time 30 \ --retry 2 \ --retry-delay 2 \ -X POST \ -H "Content-Type: application/json" \ + -H "X-Gitea-Event: push" \ + -H "X-Gogs-Event: push" \ + -H "X-GitHub-Event: push" \ "${AUTH_HEADER[@]}" \ --data "${PAYLOAD}" \ - "${webhook_url}" + "${webhook_url}") + printf '%s\n' "${RESPONSE}" + if printf '%s' "${RESPONSE}" | grep -qi "Branch Not Match"; then + echo "Dokploy rejected ${service_name}: branch did not match" >&2 + exit 1 + fi } triggered=0 @@ -491,9 +566,9 @@ jobs: } >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}" cleanup_dev_database: - name: Cleanup Dev Database + name: Drop and Recreate Dev Database runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 if: | github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev' && @@ -528,28 +603,12 @@ jobs: "${APT_RUNNER[@]}" apt-get update "${APT_RUNNER[@]}" apt-get install -y postgresql-client - - name: Recreate UTF8 database or reset public schema + - name: Drop and recreate UTF8 database 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 \ + DB_EXISTS=$(psql \ --set ON_ERROR_STOP=1 \ --host="${POSTGRES_HOST}" \ --port="${POSTGRES_PORT}" \ @@ -558,11 +617,13 @@ jobs: --tuples-only \ --no-align \ --set=dbname="${POSTGRES_DB}" \ - --command="SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = :'dbname';") + <<'SQL' + SELECT 1 FROM pg_database WHERE datname = :'dbname'; + SQL + ) - if [ "${ENCODING:-}" != "UTF8" ]; then - echo "Database ${POSTGRES_DB} encoding is ${ENCODING:-missing}; recreating as UTF8" - terminate_connections + if [ "${DB_EXISTS:-}" = "1" ]; then + echo "Closing active connections to ${POSTGRES_DB}" psql \ --set ON_ERROR_STOP=1 \ --host="${POSTGRES_HOST}" \ @@ -570,30 +631,30 @@ jobs: --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; + ALTER DATABASE :"dbname" WITH ALLOW_CONNECTIONS false; + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = :'dbname' + AND pid <> pg_backend_pid(); SQL fi + echo "Dropping and recreating ${POSTGRES_DB} with UTF8 encoding" 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 + + DB_ENCODING=$(psql \ --set ON_ERROR_STOP=1 \ --host="${POSTGRES_HOST}" \ --port="${POSTGRES_PORT}" \ @@ -602,19 +663,118 @@ jobs: --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 + <<'SQL' + SELECT pg_encoding_to_char(encoding) + FROM pg_database + WHERE datname = :'dbname'; + SQL + ) - if [ "$(cat /tmp/mostovik-db-encoding)" != "UTF8" ]; then + printf '%s\n' "${DB_ENCODING}" | tee /tmp/mostovik-db-encoding + + if [ "${DB_ENCODING}" != "UTF8" ]; then echo "Database ${POSTGRES_DB} is not UTF8 after cleanup" >&2 exit 1 fi + - name: Trigger Dokploy after database recreate + 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 + + repository = os.environ.get("GITHUB_REPOSITORY", "") + repository_name = repository.rsplit("/", 1)[-1] + branch = os.environ.get("GITHUB_REF_NAME") or "dev" + sha = os.environ.get("GITHUB_SHA") or "" + server_url = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") + repository_url = f"{server_url}/{repository}" if server_url and repository else "" + celery_image = f"{os.environ['REGISTRY_HOST']}/{os.environ['REGISTRY_NAMESPACE']}/{os.environ['CELERY_IMAGE']}:dev" + payload = { + "ref": f"refs/heads/{branch}", + "after": sha, + "checkout_sha": sha, + "repository": { + "name": repository_name, + "full_name": repository, + "html_url": repository_url, + "clone_url": f"{repository_url}.git" if repository_url else "", + }, + "sender": {"login": os.environ.get("GITHUB_ACTOR")}, + "pusher": {"name": os.environ.get("GITHUB_ACTOR")}, + "head_commit": { + "id": sha, + "message": f"CI deploy {os.environ.get('CURRENT_DOKPLOY_TARGET')}", + "url": f"{repository_url}/commit/{sha}" if repository_url and sha else "", + }, + "commits": [ + { + "id": sha, + "message": f"CI deploy {os.environ.get('CURRENT_DOKPLOY_TARGET')}", + "url": f"{repository_url}/commit/{sha}" if repository_url and sha else "", + } + ], + "project": repository, + "branch": branch, + "sha": 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}" + RESPONSE=$(curl -fsS \ + --connect-timeout 5 \ + --max-time 30 \ + --retry 2 \ + --retry-delay 2 \ + -X POST \ + -H "Content-Type: application/json" \ + -H "X-Gitea-Event: push" \ + -H "X-Gogs-Event: push" \ + -H "X-GitHub-Event: push" \ + "${AUTH_HEADER[@]}" \ + --data "${PAYLOAD}" \ + "${webhook_url}") + printf '%s\n' "${RESPONSE}" + if printf '%s' "${RESPONSE}" | grep -qi "Branch Not Match"; then + echo "Dokploy rejected ${service_name}: branch did not match" >&2 + exit 1 + fi + } + + call_webhook "dev web" "${DOKPLOY_DEV_WEB_WEBHOOK_URL}" "web" + sleep 45 + call_webhook "dev worker" "${DOKPLOY_DEV_WORKER_WEBHOOK_URL}" "worker" + call_webhook "dev beat" "${DOKPLOY_DEV_BEAT_WEBHOOK_URL}" "beat" + - name: Summary run: | set -euo pipefail { - echo "Dev database cleanup completed." + echo "Dev database was dropped and recreated." echo "Database: ${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" echo "Encoding: UTF8" + echo "Dokploy web/worker/beat deploy was triggered." } >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"