diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8472ba6..6def7db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,143 +1,71 @@ -name: Deploy docker +name: Deploy redirector on: push: branches: ['main'] - tags: - - 'v*' - + tags: ['v*'] env: - REGISTRY: ghcr.io - API_IMAGE_NAME: dyakovri/redirector-api - UI_IMAGE_NAME: dyakovri/redirector-ui - API_CONTAITER_NAME: com_profcomff_api_redirect - UI_CONTAITER_NAME: com_profcomff_ui_redirect + REGISTRY: git.dyakov.space jobs: - build-testing: - name: Build testing - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - repository: ${{ env.UI_IMAGE_NAME }} - - name: Log in to the Container registry - uses: docker/login-action@v2 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/profcomff/redirect-ui - tags: | - type=raw,value=test,enable=true - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - API_ROOT=https://to.test.profcomff.com - deploy-testing: name: Deploy Testing + if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: [self-hosted, Linux, testing] - needs: build-testing environment: name: Testing url: https://to.test.profcomff.com/ permissions: packages: read - + env: + PROJECT_VERSION: dev-latest + PROJECT_NAME: com_profcomff_redirect_test + CONTAINER_SUFFIX: _test + DB_DSN: ${{ secrets.DB_DSN }} + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }} + BASE_URL: ${{ vars.BASE_URL }} + OIDC_CONFIGURATION_URI: ${{ vars.OIDC_CONFIGURATION_URI }} + OIDC_CLIENT_ID: ${{ vars.OIDC_CLIENT_ID }} + OIDC_ADMIN_CLAIM: ${{ vars.OIDC_ADMIN_CLAIM }} + OIDC_ADMIN_CLAIM_VALUE: ${{ vars.OIDC_ADMIN_CLAIM_VALUE }} + ALLOWED_DOMAINS: ${{ vars.ALLOWED_DOMAINS }} steps: - - name: Pull new API - run: docker pull ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:master + - name: Checkout repository + uses: actions/checkout@v4 - - name: Pull new UI - run: docker pull ${{ env.REGISTRY }}/profcomff/redirect-ui:test + - name: Log in to git.dyakov.space + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: robot-profcomff + password: ${{ secrets.DYAKOVSPACE_CI_TOKEN }} - - name: Migrate DB + - name: Pull images run: | - docker run \ - --rm \ - --network=web \ - --env DB_DSN=${{ secrets.DB_DSN }} \ - --name ${{ env.API_CONTAITER_NAME }}_migration \ - ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:master \ - alembic upgrade head + docker compose \ + --project-directory deploy \ + --project-name ${{ env.PROJECT_NAME }} \ + pull - - name: Run test API + - name: Migrate DB run: | - docker stop ${{ env.API_CONTAITER_NAME }}_test || true && docker rm ${{ env.API_CONTAITER_NAME }}_test || true - docker run \ - --detach \ - --restart on-failure:3 \ - --network=web \ - --env DB_DSN=${{ secrets.DB_DSN }} \ - --name ${{ env.API_CONTAITER_NAME }}_test \ - ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:master + docker compose \ + --project-directory deploy \ + --project-name ${{ env.PROJECT_NAME }} \ + run --rm control-panel-service \ + uv run --active --no-sync --directory=backend/control-panel-service alembic upgrade head - - name: Run test UI + - name: Deploy run: | - docker stop ${{ env.UI_CONTAITER_NAME }}_test || true && docker rm ${{ env.UI_CONTAITER_NAME }}_test || true - docker run \ - --detach \ - --restart on-failure:3 \ - --network=web \ - --name ${{ env.UI_CONTAITER_NAME }}_test \ - ${{ env.REGISTRY }}/profcomff/redirect-ui:test - - build-production: - name: Build production - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - repository: ${{ env.UI_IMAGE_NAME }} - - name: Log in to the Container registry - uses: docker/login-action@v2 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/profcomff/redirect-ui - tags: | - type=ref,event=tag,enable=${{ startsWith(github.ref, 'refs/tags/v') }} - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - API_ROOT=https://to.profcomff.com + docker compose \ + --project-directory deploy \ + --project-name ${{ env.PROJECT_NAME }} \ + up --detach --remove-orphans deploy-production: name: Deploy Production - needs: - - build-production - - deploy-testing if: startsWith(github.ref, 'refs/tags/v') runs-on: [self-hosted, Linux, production] environment: @@ -145,42 +73,48 @@ jobs: url: https://to.profcomff.com/ permissions: packages: read - + env: + PROJECT_VERSION: latest + PROJECT_NAME: com_profcomff_redirect + CONTAINER_SUFFIX: "" + DB_DSN: ${{ secrets.DB_DSN }} + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }} + BASE_URL: ${{ vars.BASE_URL }} + OIDC_CONFIGURATION_URI: ${{ vars.OIDC_CONFIGURATION_URI }} + OIDC_CLIENT_ID: ${{ vars.OIDC_CLIENT_ID }} + OIDC_ADMIN_CLAIM: ${{ vars.OIDC_ADMIN_CLAIM }} + OIDC_ADMIN_CLAIM_VALUE: ${{ vars.OIDC_ADMIN_CLAIM_VALUE }} + ALLOWED_DOMAINS: ${{ vars.ALLOWED_DOMAINS }} steps: - - name: Pull new API - run: docker pull ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:master + - name: Checkout repository + uses: actions/checkout@v4 - - name: Pull new UI - run: docker pull ${{ env.REGISTRY }}/profcomff/redirect-ui:latest + - name: Log in to git.dyakov.space + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: robot-profcomff + password: ${{ secrets.DYAKOVSPACE_CI_TOKEN }} - - name: Migrate DB + - name: Pull images run: | - docker run \ - --rm \ - --network=web \ - --env DB_DSN=${{ secrets.DB_DSN }} \ - --name ${{ env.API_CONTAITER_NAME }}_migration \ - ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:master \ - alembic upgrade head + docker compose \ + --project-directory deploy \ + --project-name ${{ env.PROJECT_NAME }} \ + pull - - name: Run test API + - name: Migrate DB run: | - docker stop ${{ env.API_CONTAITER_NAME }} || true && docker rm ${{ env.API_CONTAITER_NAME }} || true - docker run \ - --detach \ - --restart always \ - --network=web \ - --env DB_DSN=${{ secrets.DB_DSN }} \ - --env SECRET=${{ secrets.SECRET }} \ - --name ${{ env.API_CONTAITER_NAME }} \ - ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:master + docker compose \ + --project-directory deploy \ + --project-name ${{ env.PROJECT_NAME }} \ + run --rm control-panel-service \ + uv run --active --no-sync --directory=backend/control-panel-service alembic upgrade head - - name: Run test UI + - name: Deploy run: | - docker stop ${{ env.UI_CONTAITER_NAME }} || true && docker rm ${{ env.UI_CONTAITER_NAME }} || true - docker run \ - --detach \ - --restart always \ - --network=web \ - --name ${{ env.UI_CONTAITER_NAME }} \ - ${{ env.REGISTRY }}/profcomff/redirect-ui:latest + docker compose \ + --project-directory deploy \ + --project-name ${{ env.PROJECT_NAME }} \ + up --detach --remove-orphans diff --git a/README.md b/README.md index 47c4a85..bb75c5d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,95 @@ -# Сервис для сокращения ссылок +# Сервис коротких ссылок -Превращаем страшненькие ссылки в ссылки вида https://to.profcomff.com +Превращает длинные ссылки в `https://to.profcomff.com/`. OIDC-аутентификация +через `auth.profcomff.com`, soft-delete ссылок с сохранением истории переходов, +бар-чарты переходов за час/день/всё время. -Это только деплой приложения, сам код приложения можно глянуть тут: +- Production: +- Testing: -- API https://github.com/dyakovri/redirector-api -- Webapp https://github.com/dyakovri/redirector-ui +## Стек + +- **API:** Python 3.13 + FastAPI + SQLAlchemy/alembic. Внешний Postgres (по `DB_DSN`). +- **Web:** Vite + React 19 + TypeScript + Tailwind 4 SPA (PWA). +- **Auth:** HS256 access-JWT (15 мин, in-memory у фронта) + HttpOnly refresh-cookie + (sliding 30 дней). OIDC-флоу через `/api/v2/oidc/redirect` + `/api/v2/oidc/callback`. +- **Инфра:** Docker Compose поверх существующей external-сети `web`. Reverse-proxy + на хосте. + +Исходники сервиса закрытые; в этом репо живёт только деплой-обвязка. + +## Что в репо + +``` +deploy/compose.yaml prod docker-compose (API + UI, БД внешняя) +.github/workflows/deploy.yml GitHub Actions: testing + production +``` + +Образы тянутся из `git.dyakov.space/dyakov-space/to/{control-panel-service,web}` +(приватный Gitea-registry, доступ через `secrets.DYAKOVSPACE_CI_TOKEN`). + +## Деплой + +| Триггер | Окружение | Runner | Тег образов | Project name | +|---------|-----------|--------|-------------|--------------| +| push в `main` | Testing | `[self-hosted, Linux, testing]` | `dev-latest` | `com_profcomff_redirect_test` | +| push тега `v*` | Production | `[self-hosted, Linux, production]` | `latest` | `com_profcomff_redirect` | + +Шаги одинаковые: `docker compose pull` → `docker compose run --rm +control-panel-service ... alembic upgrade head` → `docker compose up -d +--remove-orphans`. Обе среды мигрируют свою БД отдельно. + +Имена контейнеров (`com_profcomff_api_redirect[_test]`, +`com_profcomff_ui_redirect[_test]`) сохранены ради существующих reverse-proxy +конфигов. Дополнительно сервисы получают network-aliases `redirector-api` и +`redirector-www` на сети `web`. + +## Требования к окружению на раннере + +- `docker` + плагин `docker compose` v2. +- Существующая external network `web` (`docker network create web`). +- Доступ к Postgres-инстансу, который указан в `DB_DSN` для этой среды. + +## Переменные в GitHub Environments + +Задаются вручную в Settings → Environments → Testing/Production. + +**Secrets** (одинаковый набор в обоих окружениях, разные значения): + +| Имя | Назначение | +|-----|-----------| +| `DYAKOVSPACE_CI_TOKEN` | Read-токен к `git.dyakov.space` для `docker login` | +| `DB_DSN` | `postgresql://...` к внешней БД (своя на каждом окружении) | +| `JWT_SECRET_KEY` | HS256-ключ подписи наших access-токенов (`openssl rand -hex 32`) | +| `OIDC_CLIENT_SECRET` | OIDC client secret | +| `OIDC_TRUSTED_TOKEN` | Опционально. Bearer-bypass для dev. В проде оставлять пустым | + +**Vars:** + +| Имя | Пример (production) | +|-----|--------------------| +| `BASE_URL` | `https://to.profcomff.com` | +| `OIDC_CONFIGURATION_URI` | `https://auth.profcomff.com/application/o/redirector/.well-known/openid-configuration` | +| `OIDC_CLIENT_ID` | `redirector` | +| `OIDC_ADMIN_CLAIM` | `groups` | +| `OIDC_ADMIN_CLAIM_VALUE` | `redirector-admin` | +| `ALLOWED_DOMAINS` | `["https://to.profcomff.com"]` | + +## Ручной запуск на раннере + +```bash +cd /path/to/checkout +PROJECT_VERSION=latest CONTAINER_SUFFIX= \ +DB_DSN=... BASE_URL=... JWT_SECRET_KEY=... \ +OIDC_CONFIGURATION_URI=... OIDC_CLIENT_ID=... OIDC_CLIENT_SECRET=... \ +OIDC_ADMIN_CLAIM=groups OIDC_ADMIN_CLAIM_VALUE='Redirector Admin' \ +ALLOWED_DOMAINS='["https://to.profcomff.com"]' \ +docker compose --project-directory deploy --project-name com_profcomff_redirect pull + +docker compose --project-directory deploy --project-name com_profcomff_redirect \ + run --rm control-panel-service \ + uv run --active --no-sync --directory=backend/control-panel-service alembic upgrade head + +docker compose --project-directory deploy --project-name com_profcomff_redirect \ + up --detach --remove-orphans +``` diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 0000000..13bc206 --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,33 @@ +services: + control-panel-service: + image: ${REGISTRY:-git.dyakov.space}/dyakov-space/to/control-panel-service:${PROJECT_VERSION:-latest} + restart: unless-stopped + networks: + web: + aliases: + - redirector-api + environment: + DB_DSN: ${DB_DSN} + BASE_URL: ${BASE_URL} + JWT_SECRET_KEY: ${JWT_SECRET_KEY} + OIDC_CONFIGURATION_URI: ${OIDC_CONFIGURATION_URI} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET} + OIDC_ADMIN_CLAIM: ${OIDC_ADMIN_CLAIM} + OIDC_ADMIN_CLAIM_VALUE: ${OIDC_ADMIN_CLAIM_VALUE} + ALLOWED_DOMAINS: ${ALLOWED_DOMAINS} + + web: + image: ${REGISTRY:-git.dyakov.space}/dyakov-space/to/web:${PROJECT_VERSION:-latest} + restart: unless-stopped + networks: + web: + aliases: + - redirector-www + depends_on: + - control-panel-service + +networks: + web: + external: true + name: web