Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 78 additions & 144 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,186 +1,120 @@
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:
name: Production
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
97 changes: 92 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,95 @@
# Сервис для сокращения ссылок
# Сервис коротких ссылок

Превращаем страшненькие ссылки в ссылки вида https://to.profcomff.com
Превращает длинные ссылки в `https://to.profcomff.com/<alias>`. OIDC-аутентификация
через `auth.profcomff.com`, soft-delete ссылок с сохранением истории переходов,
бар-чарты переходов за час/день/всё время.

Это только деплой приложения, сам код приложения можно глянуть тут:
- Production: <https://to.profcomff.com/>
- Testing: <https://to.test.profcomff.com/>

- 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
```
33 changes: 33 additions & 0 deletions deploy/compose.yaml
Original file line number Diff line number Diff line change
@@ -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