From 2c12b4940dabf4898b64607364e9b5dddab409b5 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 12 Apr 2026 21:32:10 +0500 Subject: [PATCH] Add runner-based auto deploy for marketing-parser --- .gitlab-ci.yml | 30 +++++++ DEPLOYMENT.md | 45 ++++++++++ deployment/docker-compose.server.yml | 22 +++++ scripts/bootstrap_gitlab_deploy.sh | 59 ++++++++++++++ scripts/deploy_gitlab.sh | 118 +++++++++++++++++++++++++++ scripts/install_gitlab_runner.sh | 72 ++++++++++++++++ 6 files changed, 346 insertions(+) create mode 100644 DEPLOYMENT.md create mode 100644 deployment/docker-compose.server.yml create mode 100755 scripts/bootstrap_gitlab_deploy.sh create mode 100755 scripts/deploy_gitlab.sh create mode 100755 scripts/install_gitlab_runner.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2e23839..161e7e0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,9 +2,13 @@ image: eclipse-temurin:21-jdk variables: MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" + DEPLOY_DIR: "/home/gitlab-runner/deploy/marketing-parser" + PARSER_IMAGE_NAME: "marketing-parser-app" + PARSER_CONTAINER_NAME: "konturai-parser-app" stages: - test + - deploy cache: key: ${CI_COMMIT_REF_SLUG} @@ -24,3 +28,29 @@ test: reports: junit: - target/surefire-reports/TEST-*.xml + +deploy_production: + stage: deploy + needs: + - test + tags: + - marketing-parser-prod + variables: + DOCKER_HOST: "" + DOCKER_TLS_VERIFY: "" + DOCKER_CERT_PATH: "" + DOCKER_TLS_CERTDIR: "" + COMPOSE_FILE: "$DEPLOY_DIR/deployment/docker-compose.server.yml" + COMPOSE_PROJECT_NAME: "marketing-parser" + rules: + - if: '$CI_PIPELINE_SOURCE == "push"' + - if: '$CI_PIPELINE_SOURCE == "web"' + before_script: + - command -v bash >/dev/null + - command -v docker >/dev/null + - command -v rsync >/dev/null + script: + - bash scripts/deploy_gitlab.sh + environment: + name: production + resource_group: marketing-parser-production diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..36176f8 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,45 @@ +# Deployment + +This repository now uses the same GitLab shell-runner deployment pattern as `call-center`: no project CI/CD variables are required for the normal push deploy path. + +## What happens on push + +1. GitLab runs the existing `test` job. +2. A dedicated shell runner with tag `marketing-parser-prod` picks up `deploy_production`. +3. The runner syncs the repository into `/home/gitlab-runner/deploy/marketing-parser`. +4. The runner builds a local Docker image from that synced checkout. +5. Docker Compose recreates `konturai-parser-app` from [deployment/docker-compose.server.yml](/root/marketing-parser/deployment/docker-compose.server.yml:1). + +## Files used + +- [.gitlab-ci.yml](/root/marketing-parser/.gitlab-ci.yml:1) +- [deployment/docker-compose.server.yml](/root/marketing-parser/deployment/docker-compose.server.yml:1) +- [scripts/deploy_gitlab.sh](/root/marketing-parser/scripts/deploy_gitlab.sh:1) +- [scripts/bootstrap_gitlab_deploy.sh](/root/marketing-parser/scripts/bootstrap_gitlab_deploy.sh:1) +- [scripts/install_gitlab_runner.sh](/root/marketing-parser/scripts/install_gitlab_runner.sh:1) + +## One-time server setup + +Install and register a shell runner on the target server: + +```bash +cd /root/marketing-parser +RUNNER_TOKEN=glrt-xxxxxxxx bash scripts/install_gitlab_runner.sh +``` + +Then bootstrap the production env file from the current `konturai-parser-app` container: + +```bash +cd /root/marketing-parser +bash scripts/bootstrap_gitlab_deploy.sh +``` + +That writes `/home/gitlab-runner/deploy/marketing-parser/.env.production`, which is used by the deploy job. + +## Important notes + +- The deploy job runs for pushes and manual web-triggered pipelines on the branch that contains this CI configuration. +- The runner user must have Docker access. +- This setup expects a host `shell` runner. A minimal `gitlab/gitlab-runner:alpine` container is not enough unless you also provide Docker, Compose, and `rsync` inside that runner environment. +- This deploy path is intentionally independent from `/root/docker-compose.yaml` because `/root` is not accessible to the `gitlab-runner` user. +- The deployment compose file joins the existing external Docker network `common_network` and publishes the service alias `parser-service`. diff --git a/deployment/docker-compose.server.yml b/deployment/docker-compose.server.yml new file mode 100644 index 0000000..fac2860 --- /dev/null +++ b/deployment/docker-compose.server.yml @@ -0,0 +1,22 @@ +name: marketing-parser + +services: + parser-service: + build: + context: .. + dockerfile: Dockerfile + image: ${PARSER_IMAGE:-marketing-parser-app:latest} + container_name: ${PARSER_CONTAINER_NAME:-konturai-parser-app} + restart: unless-stopped + env_file: + - ../.env.production + extra_hosts: + - host.docker.internal:host-gateway + networks: + common_network: + aliases: + - parser-service + +networks: + common_network: + external: true diff --git a/scripts/bootstrap_gitlab_deploy.sh b/scripts/bootstrap_gitlab_deploy.sh new file mode 100755 index 0000000..de63154 --- /dev/null +++ b/scripts/bootstrap_gitlab_deploy.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SOURCE_CONTAINER="${SOURCE_CONTAINER:-konturai-parser-app}" +DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/marketing-parser}" + +ENV_KEYS=( + SPRING_DATASOURCE_URL + POSTGRES_USER + POSTGRES_PASSWORD + SPRING_JPA_HIBERNATE_DDL_AUTO + CORS_ALLOWED_ORIGIN + FACEBOOK_LEADS_PAGE_ID + FACEBOOK_LEADS_ACCESS_TOKEN + FACEBOOK_LEADS_CRON + FACEBOOK_LEADS_GRAPH_API_VERSION + FACEBOOK_LEADS_CALL_CENTER_BASE_URL +) + +if [[ "${EUID}" -ne 0 ]]; then + echo "Run this script as root so it can write the deploy directory for gitlab-runner." >&2 + exit 1 +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "Docker is required." >&2 + exit 1 +fi + +if ! id gitlab-runner >/dev/null 2>&1; then + echo "gitlab-runner user does not exist." >&2 + exit 1 +fi + +if ! docker inspect "$SOURCE_CONTAINER" >/dev/null 2>&1; then + echo "Source container not found: $SOURCE_CONTAINER" >&2 + exit 1 +fi + +install -d -o gitlab-runner -g gitlab-runner "$DEPLOY_DIR" + +TMP_ENV="$(mktemp)" +trap 'rm -f "$TMP_ENV"' EXIT + +PATTERN="$(printf '%s|' "${ENV_KEYS[@]}")" +PATTERN="^(${PATTERN%|})=" + +docker inspect "$SOURCE_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' \ + | grep -E "$PATTERN" > "$TMP_ENV" + +if [[ ! -s "$TMP_ENV" ]]; then + echo "Failed to extract deployment environment from $SOURCE_CONTAINER" >&2 + exit 1 +fi + +install -m 600 -o gitlab-runner -g gitlab-runner "$TMP_ENV" "$DEPLOY_DIR/.env.production" + +echo "Bootstrap complete: wrote $DEPLOY_DIR/.env.production from $SOURCE_CONTAINER" diff --git a/scripts/deploy_gitlab.sh b/scripts/deploy_gitlab.sh new file mode 100755 index 0000000..7fa0481 --- /dev/null +++ b/scripts/deploy_gitlab.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/marketing-parser}" +COMPOSE_FILE="${COMPOSE_FILE:-$DEPLOY_DIR/deployment/docker-compose.server.yml}" +PARSER_IMAGE_NAME="${PARSER_IMAGE_NAME:-marketing-parser-app}" +PARSER_IMAGE_TAG="${PARSER_IMAGE_TAG:-${CI_COMMIT_SHORT_SHA:-latest}}" +PARSER_IMAGE="${PARSER_IMAGE:-${PARSER_IMAGE_NAME}:${PARSER_IMAGE_TAG}}" +PARSER_CONTAINER_NAME="${PARSER_CONTAINER_NAME:-konturai-parser-app}" +COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-marketing-parser}" +WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-120}" + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +wait_for_container() { + local deadline status + deadline=$((SECONDS + WAIT_TIMEOUT_SECONDS)) + + while (( SECONDS < deadline )); do + status="$(docker inspect "$PARSER_CONTAINER_NAME" --format '{{.State.Status}}' 2>/dev/null || true)" + if [[ "$status" == "running" ]]; then + echo "Container is running: $PARSER_CONTAINER_NAME" + return 0 + fi + if [[ "$status" == "exited" || "$status" == "dead" ]]; then + echo "Container $PARSER_CONTAINER_NAME is in state: $status" >&2 + docker logs --tail 100 "$PARSER_CONTAINER_NAME" || true + exit 1 + fi + sleep 3 + done + + echo "Timed out waiting for $PARSER_CONTAINER_NAME to start" >&2 + docker logs --tail 100 "$PARSER_CONTAINER_NAME" || true + exit 1 +} + +require_command bash +require_command docker +require_command rsync + +if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD=(docker compose) +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD=(docker-compose) +else + echo "Missing docker compose or docker-compose" >&2 + exit 1 +fi + +if [[ -z "${CI_PROJECT_DIR:-}" || ! -d "${CI_PROJECT_DIR:-}" ]]; then + echo "CI_PROJECT_DIR is not set or does not exist" >&2 + exit 1 +fi + +if ! docker info >/dev/null 2>&1; then + echo "Docker daemon is not reachable for the runner user" >&2 + exit 1 +fi + +install -d "$DEPLOY_DIR" + +if [[ "$CI_PROJECT_DIR" != "$DEPLOY_DIR" ]]; then + rsync -a --delete \ + --exclude '.git/' \ + --exclude '.m2/' \ + --exclude 'target/' \ + --exclude '.codex' \ + --exclude '.claude/' \ + --exclude '.cursor/' \ + --exclude '.idea/' \ + --exclude '.vscode/' \ + --exclude '.DS_Store' \ + --exclude '.env' \ + --exclude '.env.*' \ + "$CI_PROJECT_DIR/" "$DEPLOY_DIR/" +fi + +if [[ ! -f "$DEPLOY_DIR/.env.production" ]]; then + if [[ -n "${DEPLOY_ENV_FILE:-}" && -f "${DEPLOY_ENV_FILE}" ]]; then + install -m 600 "$DEPLOY_ENV_FILE" "$DEPLOY_DIR/.env.production" + else + echo "Missing $DEPLOY_DIR/.env.production. Run scripts/bootstrap_gitlab_deploy.sh once or set DEPLOY_ENV_FILE." >&2 + exit 1 + fi +fi + +if docker ps -a --format '{{.Names}}' | grep -Fxq "$PARSER_CONTAINER_NAME"; then + EXISTING_PROJECT="$(docker inspect "$PARSER_CONTAINER_NAME" --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)" + if [[ -n "$EXISTING_PROJECT" && "$EXISTING_PROJECT" != "$COMPOSE_PROJECT_NAME" ]]; then + echo "Removing existing container $PARSER_CONTAINER_NAME from compose project $EXISTING_PROJECT" + docker rm -f "$PARSER_CONTAINER_NAME" + fi +fi + +cd "$DEPLOY_DIR" + +echo "Deploy directory: $DEPLOY_DIR" +echo "Building image: $PARSER_IMAGE" + +docker build -t "$PARSER_IMAGE" -t "${PARSER_IMAGE_NAME}:latest" "$DEPLOY_DIR" + +export PARSER_IMAGE +export PARSER_CONTAINER_NAME +export COMPOSE_PROJECT_NAME + +"${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" config -q +"${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" up -d --remove-orphans --force-recreate + +wait_for_container + +docker ps --filter "name=^${PARSER_CONTAINER_NAME}$" --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' diff --git a/scripts/install_gitlab_runner.sh b/scripts/install_gitlab_runner.sh new file mode 100755 index 0000000..d6e4c47 --- /dev/null +++ b/scripts/install_gitlab_runner.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +GITLAB_URL="${GITLAB_URL:-https://gitlab.konturai.kz/}" +RUNNER_NAME="${RUNNER_NAME:-marketing-parser-prod-runner}" +RUNNER_TAGS="${RUNNER_TAGS:-marketing-parser-prod}" +RUNNER_EXECUTOR="${RUNNER_EXECUTOR:-shell}" +RUNNER_TOKEN="${RUNNER_TOKEN:-}" +DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/marketing-parser}" +SKIP_REGISTER="${SKIP_REGISTER:-0}" + +if [[ "${EUID}" -ne 0 ]]; then + echo "Run this script as root." >&2 + exit 1 +fi + +if [[ "$SKIP_REGISTER" != "1" && -z "$RUNNER_TOKEN" ]]; then + echo "Set RUNNER_TOKEN from GitLab project/group runner settings before running this script." >&2 + exit 1 +fi + +apt-get update +apt-get install -y ca-certificates curl rsync docker.io docker-compose-plugin + +if ! command -v gitlab-runner >/dev/null 2>&1; then + curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash + apt-get install -y gitlab-runner +fi + +if ! getent group docker >/dev/null 2>&1; then + echo "Docker group does not exist. Install Docker before registering the runner." >&2 + exit 1 +fi + +usermod -aG docker gitlab-runner +install -d -o gitlab-runner -g gitlab-runner "$(dirname "$DEPLOY_DIR")" +install -d -o gitlab-runner -g gitlab-runner "$DEPLOY_DIR" + +if [[ "$SKIP_REGISTER" == "1" ]]; then + echo "gitlab-runner installed. Registration skipped because SKIP_REGISTER=1." +elif gitlab-runner list 2>/dev/null | grep -Fq "$RUNNER_NAME"; then + echo "Runner $RUNNER_NAME is already registered." +else + register_args=( + --non-interactive + --url "$GITLAB_URL" + --executor "$RUNNER_EXECUTOR" + --description "$RUNNER_NAME" + --tag-list "$RUNNER_TAGS" + --run-untagged="false" + --locked="true" + ) + if [[ "$RUNNER_TOKEN" == glrt-* ]]; then + register_args+=(--token "$RUNNER_TOKEN") + else + register_args+=(--registration-token "$RUNNER_TOKEN") + fi + gitlab-runner register "${register_args[@]}" +fi + +systemctl enable --now gitlab-runner +systemctl restart gitlab-runner +if [[ "$SKIP_REGISTER" != "1" ]]; then + gitlab-runner verify +fi + +if [[ "$SKIP_REGISTER" == "1" ]]; then + echo "gitlab-runner service is installed and running." +else + echo "Runner $RUNNER_NAME is ready." +fi