From 1edda33f87646e85043cc2c4ead814d7d222d45a Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 4 Aug 2026 15:16:33 -0400 Subject: [PATCH] =?UTF-8?q?ci(site):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20Git?= =?UTF-8?q?ea=20Actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - основной сайт не должен зависеть от заблокированной учётной записи GitHub. - Что: - добавлены строгая сборка и атомарная публикация через repository-scoped runner. - добавлены воспроизводимые конфигурации systemd, nginx и эксплуатационный runbook. - проектная документация и ссылки на репозиторий обновлены для Gitea. - Проверка: - выполнены mkdocs build --strict, Bash/YAML-проверки и локальный HTTP smoke-check. - runner зарегистрирован, ограничен средствами systemd и виден в Gitea как online. --- .gitea/scripts/deploy-site.sh | 75 +++++++++ .gitea/workflows/deploy-site.yml | 46 +++++ AGENTS.md | 11 +- mkdocs.yml | 11 +- project/PRD.md | 10 +- project/TODO.md | 4 + project/ops/gitea-vps-site/README.md | 157 ++++++++++++++++++ .../ops/gitea-vps-site/gitea-runner.service | 47 ++++++ project/ops/gitea-vps-site/gitea-runner.yaml | 37 +++++ project/ops/gitea-vps-site/nginx.conf | 21 +++ 10 files changed, 408 insertions(+), 11 deletions(-) create mode 100755 .gitea/scripts/deploy-site.sh create mode 100644 .gitea/workflows/deploy-site.yml create mode 100644 project/ops/gitea-vps-site/README.md create mode 100644 project/ops/gitea-vps-site/gitea-runner.service create mode 100644 project/ops/gitea-vps-site/gitea-runner.yaml create mode 100644 project/ops/gitea-vps-site/nginx.conf diff --git a/.gitea/scripts/deploy-site.sh b/.gitea/scripts/deploy-site.sh new file mode 100755 index 0000000..b74df97 --- /dev/null +++ b/.gitea/scripts/deploy-site.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly deploy_root="/srv/de-roadmap" +readonly releases_root="${deploy_root}/releases" +readonly keep_releases=3 + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 SITE_DIR COMMIT_SHA-RUN_ID" >&2 + exit 2 +fi + +source_dir=$(realpath "$1") +release_id=$2 + +if [[ ! $release_id =~ ^[0-9a-f]{40}-[0-9]+$ ]]; then + echo "Invalid release id: ${release_id}" >&2 + exit 2 +fi + +if [[ ! -f "${source_dir}/index.html" ]]; then + echo "Built site has no index.html: ${source_dir}" >&2 + exit 2 +fi + +if [[ ! -d $releases_root || ! -w $releases_root || ! -w $deploy_root ]]; then + echo "Deployment directories are missing or not writable" >&2 + exit 1 +fi + +readonly release_dir="${releases_root}/${release_id}" +readonly staging_dir="${releases_root}/.${release_id}.tmp" +readonly next_link="${deploy_root}/.current.${release_id}.tmp" + +if [[ -e $release_dir || -e $staging_dir || -e $next_link ]]; then + echo "Release path already exists: ${release_id}" >&2 + exit 1 +fi + +cleanup() { + rm -rf -- "$staging_dir" + rm -f -- "$next_link" +} +trap cleanup EXIT + +umask 0022 +mkdir "$staging_dir" +cp -a "${source_dir}/." "$staging_dir/" +chmod -R u=rwX,go=rX "$staging_dir" +mv "$staging_dir" "$release_dir" + +ln -s "releases/${release_id}" "$next_link" +mv -Tf "$next_link" "${deploy_root}/current" + +mapfile -t old_releases < <( + find "$releases_root" \ + -mindepth 1 \ + -maxdepth 1 \ + -type d \ + -regextype posix-extended \ + -regex '.*/[0-9a-f]{40}-[0-9]+' \ + -printf '%T@ %f\n' \ + | sort -nr \ + | awk -v keep="$keep_releases" 'NR > keep { print $2 }' +) + +for old_release in "${old_releases[@]}"; do + if [[ $old_release =~ ^[0-9a-f]{40}-[0-9]+$ && $old_release != "$release_id" ]]; then + rm -rf -- "${releases_root:?}/${old_release}" + fi +done + +trap - EXIT +echo "Published release ${release_id}" diff --git a/.gitea/workflows/deploy-site.yml b/.gitea/workflows/deploy-site.yml new file mode 100644 index 0000000..adc7f1f --- /dev/null +++ b/.gitea/workflows/deploy-site.yml @@ -0,0 +1,46 @@ +name: Deploy MkDocs to VPS + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: site-production + cancel-in-progress: false + +jobs: + deploy: + runs-on: de-roadmap-host + timeout-minutes: 15 + + steps: + - name: Check out the triggering commit + env: + COMMIT_SHA: ${{ gitea.sha }} + REPOSITORY_URL: ${{ gitea.server_url }}/${{ gitea.repository }}.git + run: | + set -euo pipefail + [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]] + test ! -e source + mkdir source + git -C source init . + git -C source remote add origin "$REPOSITORY_URL" + git -C source fetch --no-tags --depth=1 origin "$COMMIT_SHA" + test "$(git -C source rev-parse FETCH_HEAD)" = "$COMMIT_SHA" + git -C source -c advice.detachedHead=false checkout --detach FETCH_HEAD + + - name: Build the site strictly + run: | + set -euo pipefail + cd source + uv run \ + --no-project \ + --with 'mkdocs-material==9.6.14' \ + --with 'mkdocs-same-dir==0.1.3' \ + mkdocs build --strict + + - name: Publish the complete release atomically + env: + RELEASE_ID: ${{ gitea.sha }}-${{ gitea.run_id }} + run: source/.gitea/scripts/deploy-site.sh source/site "$RELEASE_ID" diff --git a/AGENTS.md b/AGENTS.md index 23c26fd..90152be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,11 @@ - Root `README.md` describes the learning roadmap (RU) and serves as the main page of the MkDocs site. - `dwh-modeling/` contains the article and demo DWH model; SQL lives in `dwh-modeling/sql` as ordered scripts `01_...sql`–`09_...sql` (07–09 are homework DDL, template and solution). - `postgres-bookings/` is a Dockerized PostgreSQL + demo “bookings” DB; start it first, then apply DWH scripts against the `demo` database. -- `mkdocs.yml` — MkDocs Material config; `docs_dir: .` (repo root = site root). Excluded dirs: `project/`, `postgres-bookings/`, `.github/`, `.claude/`. -- `.github/workflows/deploy-site.yml` — CI/CD: push to `main` → build → deploy to GitHub Pages. +- `mkdocs.yml` — MkDocs Material config; `docs_dir: .` (repo root = site root). Excluded dirs: `project/`, `postgres-bookings/`, `.gitea/`, `.github/`, `.claude/`. +- `.gitea/workflows/deploy-site.yml` — основной CI/CD: push в `main` → строгая + сборка → атомарная публикация на VPS через repository-scoped Gitea Runner. +- `.github/workflows/deploy-site.yml` — сохранённый workflow для резервной + публикации на GitHub Pages; Gitea его не исполняет. - `project/` — PRD, ADR, and TODO.md (excluded from site). `project/TODO.md` is the prioritized project backlog: check it when planning or proposing work, and mark items done there when you complete them. ## Build, Test, and Development Commands @@ -75,5 +78,7 @@ Pull requests should focus on one topic, include a brief context, list of change ## Security & Configuration Tips - Do not commit personal `.env` files or credentials; use local overrides only. +- Gitea Runner работает в host mode: не расширяйте его scope, не добавляйте + пользователя `gitea-runner` в `sudo` или `docker` и не выдавайте ему запись + вне `/var/lib/gitea-runner` и `/srv/de-roadmap`. - Demo credentials and ports in `postgres-bookings` are for local training only—never reuse them in shared or production environments. - diff --git a/mkdocs.yml b/mkdocs.yml index 3acde1c..28c09dd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,8 +3,8 @@ site_url: https://de.dementev.space/ site_description: "Роадмап по Data Engineering: SQL, Python, Airflow, Greenplum и далее" site_author: Dmitry Dementev -repo_url: https://github.com/dementev-dev/de-roadmap -repo_name: dementev-dev/de-roadmap +repo_url: https://git.dementev.space/ddmitry/de-roadmap +repo_name: ddmitry/de-roadmap docs_dir: . site_dir: site @@ -13,6 +13,7 @@ exclude_docs: | project/ postgres-bookings/ .github/ + .gitea/ .claude/ site/ AGENTS.md @@ -75,12 +76,12 @@ markdown_extensions: extra: social: + - icon: simple/gitea + link: https://git.dementev.space/ddmitry/de-roadmap + name: Gitea - icon: fontawesome/brands/telegram link: https://t.me/dementev_dev name: Написать в Telegram - - icon: fontawesome/brands/github - link: https://github.com/dementev-dev/de-roadmap - name: GitHub plugins: - same-dir diff --git a/project/PRD.md b/project/PRD.md index 6d2574b..645d971 100644 --- a/project/PRD.md +++ b/project/PRD.md @@ -8,7 +8,10 @@ ### Текущее состояние -Роадмап по Data Engineering живёт как GitHub-репозиторий ([dementev-dev/de-roadmap](https://github.com/dementev-dev/de-roadmap)): +Роадмап по Data Engineering живёт как Git-репозиторий. Основной origin +размещён в собственной Gitea +([ddmitry/de-roadmap](https://git.dementev.space/ddmitry/de-roadmap)), а +GitHub Pages сохраняется как резерв после восстановления доступа к GitHub: - Основной контент — монолитный `README.md` (~700 строк) с полным учебным планом. - Дополнительные материалы — в подпапках (`dwh-modeling/`, `postgres-bookings/`): теория DWH-моделирования, SCD, Data Vault, домашние задания, скрипты. @@ -74,7 +77,8 @@ GitHub README — рабочий, но не презентабельный фо **Деплой:** - [x] Автоматическая сборка и публикация при пуше в `main`. -- [x] Бесплатный хостинг (GitHub Pages). +- [x] Публикация без дополнительных расходов: собственная VPS как основной + контур, GitHub Pages как резерв. **Совместимость с репо:** @@ -158,7 +162,7 @@ GitHub README — рабочий, но не презентабельный фо | Кириллические якоря рендерятся по-разному | Средняя | Среднее | Тестирование конкретного генератора; при необходимости — латинские id | | Генератор сайта перестаёт поддерживаться | Низкая | Среднее | Контент в plain Markdown — миграция на другой генератор за день | | Накладные расходы на поддержку растут | Низкая | Среднее | Принцип «сайт опционален»: если мешает — удаляем конфиг, репо работает | -| GitHub Pages ограничения (bandwidth, размер) | Очень низкая | Низкое | Для статического сайта с текстом — не актуально | +| VPS временно недоступна | Низкая | Высокое | Опубликованный сайт не зависит от Gitea; GitHub Pages сохраняется как ручной резерв | --- diff --git a/project/TODO.md b/project/TODO.md index d6e437f..50b7f1b 100644 --- a/project/TODO.md +++ b/project/TODO.md @@ -4,6 +4,10 @@ ## P1 +- [ ] **Перенести публикацию сайта на Gitea Actions и VPS.** + Настроить repository-scoped host runner, строгую сборку MkDocs, атомарные + релизы, nginx и TLS для `de.dementev.space` по согласованной спецификации. + ## P2 - [ ] **Перенос учебника Airflow в de-roadmap.** diff --git a/project/ops/gitea-vps-site/README.md b/project/ops/gitea-vps-site/README.md new file mode 100644 index 0000000..2f020c6 --- /dev/null +++ b/project/ops/gitea-vps-site/README.md @@ -0,0 +1,157 @@ +# Эксплуатация публикации `de.dementev.space` + +Этот runbook реализует спецификацию +[`2026-08-04-gitea-vps-site-publishing.md`](../../specs/2026-08-04-gitea-vps-site-publishing.md). +Команды рассчитаны на Ubuntu 26.04 и Gitea 1.27. + +## Зафиксированные параметры + +- Gitea Runner: `2.3.0`, Linux amd64. +- SHA256: `1e9fb1bea022fdf40993ecbc1a13e87db1bfd3d7f42666e37f15f71d840d53b3`. +- Runner: repository-scoped, метка `de-roadmap-host:host`. +- Пользователь сервиса: `gitea-runner`, без `sudo` и Docker. +- Корень публикации: `/srv/de-roadmap`. +- Хранение: текущий релиз и две предыдущие версии. + +## Подготовка VPS + +Установить HTTP-сервер и Certbot: + +```bash +sudo apt-get update +sudo apt-get install nginx certbot python3-certbot-nginx +``` + +Создать пользователя и каталоги: + +```bash +sudo useradd \ + --system \ + --home-dir /var/lib/gitea-runner \ + --create-home \ + --shell /usr/sbin/nologin \ + gitea-runner +sudo install -d -o gitea-runner -g gitea-runner -m 0755 \ + /var/lib/gitea-runner/workspaces \ + /srv/de-roadmap \ + /srv/de-roadmap/releases +sudo install -d -o root -g root -m 0755 /etc/gitea-runner +``` + +Скачать runner во временный каталог, сверить checksum и установить root-owned +бинарник в `/usr/local/bin/gitea-runner`: + +```bash +runner_tmp_dir=$(mktemp -d /tmp/de-roadmap-runner.XXXXXX) +curl -fsSLo "${runner_tmp_dir}/gitea-runner" \ + https://dl.gitea.com/gitea-runner/2.3.0/gitea-runner-2.3.0-linux-amd64 +printf '%s %s\n' \ + '1e9fb1bea022fdf40993ecbc1a13e87db1bfd3d7f42666e37f15f71d840d53b3' \ + "${runner_tmp_dir}/gitea-runner" \ + | sha256sum --check +sudo install -o root -g root -m 0755 \ + "${runner_tmp_dir}/gitea-runner" /usr/local/bin/gitea-runner +rm "${runner_tmp_dir}/gitea-runner" +rmdir "$runner_tmp_dir" +``` + +Из корня репозитория установить конфигурацию и unit: + +```bash +sudo install -o root -g root -m 0644 \ + project/ops/gitea-vps-site/gitea-runner.yaml \ + /etc/gitea-runner/config.yaml +sudo install -o root -g root -m 0644 \ + project/ops/gitea-vps-site/gitea-runner.service \ + /etc/systemd/system/gitea-runner.service +sudo systemd-analyze verify /etc/systemd/system/gitea-runner.service +``` + +## Регистрация runner + +Repository registration token получают в Gitea: +`ddmitry/de-roadmap` → Settings → Actions → Runners. Токен не сохраняют в Git +или shell history. Временный файл с токеном создают с владельцем +`gitea-runner:gitea-runner` и режимом `0600`. После регистрации файл +`/var/lib/gitea-runner/.runner` должен принадлежать тому же пользователю и +иметь режим `0600`. + +Token-file должен содержать ровно 40 символов без завершающего перевода строки. +При извлечении JSON-ответа через `jq` использовать `jq --join-output '.token'`, +а не `jq --raw-output`, который добавляет newline. + +```bash +sudo -u gitea-runner \ + /usr/local/bin/gitea-runner \ + --config /etc/gitea-runner/config.yaml \ + register \ + --no-interactive \ + --instance https://git.dementev.space \ + --token-file /var/lib/gitea-runner/.registration-token \ + --name de-roadmap-vps +sudo rm /var/lib/gitea-runner/.registration-token +sudo chmod 0600 /var/lib/gitea-runner/.runner +sudo systemctl daemon-reload +sudo systemctl enable --now gitea-runner +``` + +Временный файл с токеном удаляют сразу после успешной регистрации. + +## Nginx и первичная публикация + +Из корня репозитория установить virtual host, создать ссылку и только затем +перечитать проверенную конфигурацию: + +```bash +sudo install -o root -g root -m 0644 \ + project/ops/gitea-vps-site/nginx.conf \ + /etc/nginx/sites-available/de-roadmap +sudo ln -s \ + /etc/nginx/sites-available/de-roadmap \ + /etc/nginx/sites-enabled/de-roadmap +sudo nginx -t +sudo systemctl reload nginx +``` + +Default-сайт можно отключить только после успешной проверки нового virtual +host. + +До первого workflow можно собрать сайт вручную и опубликовать его тем же +скриптом с тестовым release id. Проверка до переключения DNS: + +```bash +curl --header 'Host: de.dementev.space' http://127.0.0.1/ +``` + +После локальной проверки разрешить профили `Nginx Full` в UFW. До этого +публичные порты `80/tcp` и `443/tcp` должны оставаться закрытыми. + +## DNS и TLS + +1. Уменьшить TTL записи `de.dementev.space`. +2. Направить `A` на VPS; удалить или корректно направить `AAAA`. +3. Убедиться, что сайт доступен извне по HTTP. +4. Выпустить сертификат: + + ```bash + sudo certbot --nginx -d de.dementev.space + ``` + +5. Проверить HTTPS и `systemctl status certbot.timer`. +6. Вернуть обычный DNS TTL. + +## Проверка и откат + +Активная версия определяется ссылкой `/srv/de-roadmap/current`. Для ручного +отката создать временную ссылку на нужный каталог в `releases/` и атомарно +заменить `current` через `mv -Tf`. Перед удалением релиза всегда проверять +результат `readlink -f /srv/de-roadmap/current`. + +Диагностика: + +```bash +systemctl status gitea-runner +journalctl -u gitea-runner +nginx -t +curl --header 'Host: de.dementev.space' http://127.0.0.1/ +``` diff --git a/project/ops/gitea-vps-site/gitea-runner.service b/project/ops/gitea-vps-site/gitea-runner.service new file mode 100644 index 0000000..eb71944 --- /dev/null +++ b/project/ops/gitea-vps-site/gitea-runner.service @@ -0,0 +1,47 @@ +[Unit] +Description=Gitea Actions runner for de-roadmap +Documentation=https://docs.gitea.com/usage/actions +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=gitea-runner +Group=gitea-runner +WorkingDirectory=/var/lib/gitea-runner +ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml +Restart=always +RestartSec=10s +TimeoutStopSec=45s +KillMode=mixed + +Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" +Environment="UV_CACHE_DIR=/var/lib/gitea-runner/.cache/uv" +Environment="UV_PYTHON_DOWNLOADS=never" + +UMask=0022 +NoNewPrivileges=true +PrivateDevices=true +PrivateTmp=true +ProtectClock=true +ProtectControlGroups=true +ProtectHome=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectSystem=strict +ReadWritePaths=/var/lib/gitea-runner /srv/de-roadmap +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictRealtime=true +RestrictSUIDSGID=true +LockPersonality=true +CapabilityBoundingSet= +SystemCallArchitectures=native + +MemoryMax=1G +CPUQuota=100% +TasksMax=128 + +[Install] +WantedBy=multi-user.target diff --git a/project/ops/gitea-vps-site/gitea-runner.yaml b/project/ops/gitea-vps-site/gitea-runner.yaml new file mode 100644 index 0000000..5e4e3a8 --- /dev/null +++ b/project/ops/gitea-vps-site/gitea-runner.yaml @@ -0,0 +1,37 @@ +log: + level: info + +runner: + file: /var/lib/gitea-runner/.runner + capacity: 1 + timeout: 15m + shutdown_timeout: 30s + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + fetch_interval_max: 10s + workdir_cleanup_age: 24h + idle_cleanup_interval: 10m + labels: + - "de-roadmap-host:host" + allocate_pty: false + +cache: + enabled: false + +container: + valid_volumes: [] + docker_host: "-" + require_docker: false + +host: + workdir_parent: /var/lib/gitea-runner/workspaces + +health_check: + enabled: true + min_free_disk_space_mb: 1024 + interval: 30s + timeout: 10s + +metrics: + enabled: false diff --git a/project/ops/gitea-vps-site/nginx.conf b/project/ops/gitea-vps-site/nginx.conf new file mode 100644 index 0000000..0d790ae --- /dev/null +++ b/project/ops/gitea-vps-site/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + listen [::]:80; + + server_name de.dementev.space; + root /srv/de-roadmap/current; + index index.html; + charset utf-8; + server_tokens off; + + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + location / { + try_files $uri $uri/ =404; + } + + location ~ /\. { + deny all; + } +} -- 2.54.0