Compare commits

...
2 Commits
Author SHA1 Message Date
Denozordec bb334b10f5 feat: implement versioning and release management in the project
CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 23s
CI / web (push) Successful in 28s
CI / go (push) Successful in 26s
CI / bird2 (push) Successful in 15s
- Added automatic versioning based on git tags using semantic-release in Gitea Actions.
- Introduced new API endpoints `GET /version` and `GET /v1/version` to expose build metadata.
- Updated Docker build process to include version and build time information in Go binaries.
- Enhanced documentation with details on release processes and versioning guidelines.
- Integrated version display in the web application for better user visibility.
2026-05-20 00:56:02 +07:00
Denozordec 4c23232c4e docs: update AGENTS and README to include guidelines for Conventional Commits
- Added a section on Conventional Commits in AGENTS.md, detailing the process for generating commit messages.
- Enhanced README.md with references to the Conventional Commits rules and the necessary scripts for generating commit messages.
- Clarified the format for commit messages, specifying the language requirements for headers and bodies.
2026-05-20 00:48:21 +07:00
29 changed files with 8640 additions and 190 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}
+113
View File
@@ -0,0 +1,113 @@
---
description: >-
Conventional Commits EvoBGP. Триггеры commit/коммит/закоммить/staged/commit message —
ОБЯЗАТЕЛЬНО сначала Shell scripts/commit/staged-context.ps1, скилл commit-message.
alwaysApply: false
---
# Conventional Commits (EvoBGP)
Полное описание релизного пайплайна: [docs/releasing.md](../../docs/releasing.md).
## Триггеры (применить правило + скилл)
Любой запрос на коммит или сообщение коммита: `commit`, `коммит`, `закоммить`, `git commit`, `commit message`, `conventional commit`, `staged`, «сгенерируй коммит» — в т.ч. если это указано в плане или [AGENTS.md](../../AGENTS.md).
## Обязательный запуск скрипта (MUST)
1. Прочитать скилл [`.cursor/skills/commit-message/SKILL.md`](../skills/commit-message/SKILL.md).
2. **Первым действием** выполнить Shell (из корня репо):
```powershell
powershell -NoProfile -File scripts/commit/staged-context.ps1
```
3. Сообщение коммита строить **только** по JSON из stdout скрипта (`groups`, `diff_excerpt`, `stat`).
4. **Запрещено** генерировать commit message без успешного (exit 0) запуска скрипта; не заменять скрипт одним `git diff --cached`.
## Формат (строго)
```
<type>(<scope>): <short summary in English>
<тело на русском: что изменено и зачем>
```
- **Заголовок** — только английский; императив, без точки в конце; ≤72 символов.
- **Тело** — только русский; полные предложения; пустая строка после заголовка.
- Запрещены vague-сообщения: `fix bug`, `update code`, `wip`, `misc`.
## Типы (semantic-release)
| type | Когда | Версия |
|------|--------|--------|
| `feat` | новая функциональность | minor |
| `fix` | исправление бага | patch |
| `perf` | ускорение без смены API | patch |
| `refactor` | реструктуризация без смены поведения | — |
| `docs` | только документация | — |
| `test` | тесты | — |
| `ci` | CI/CD (`.gitea/`, workflows) | — |
| `chore` | обслуживание, deps, `.cursor/` | — |
## Breaking changes
- Заголовок: `feat!` / `fix!` **или** в теле строка `BREAKING CHANGE:` (на английском ключевое слово) + описание impact **на русском**.
## Scope (EvoBGP)
Выбирать по доминирующему пути из staged diff:
| Префикс | scope |
|---------|-------|
| `internal/httpapi/` | `httpapi` |
| `internal/store/`, `internal/repository/`, `internal/db/` | `store` |
| `internal/jobs/` | `jobs` |
| `internal/pipeline/` | `pipeline` |
| `internal/birdfmt/` | `birdfmt` |
| `internal/birddeploy/` | `birddeploy` |
| `internal/bundle/`, `internal/signing/` | `bundle` |
| `cmd/` | `cmd` |
| `web/` | `web` |
| `docs/openapi.yaml`, `redocly.yaml` | `openapi` |
| `docs/` (остальное) | `docs` |
| `migrations/` | `db` |
| `.gitea/` | `ci` |
| `deploy/` | `deploy` |
| `.cursor/` | `chore` |
| прочее в корне | `chore` |
`type` определять по **содержимому diff**, не только по пути.
## Multi-change
- Несвязанные области → **отдельные коммиты** (auto-split по скиллу).
- Одна логическая фича через несколько scope (OpenAPI + httpapi + web) → **один** коммит, scope по главной области.
## Обязательный вывод агенту
Для каждого коммита:
**1. Готовое сообщение** (копировать в `git commit -m` / HEREDOC):
```
<type>(<scope>): <summary>
<тело RU>
```
**2. Пояснение (RU):** почему выбран type; риск/impact; был ли split.
## Примеры
```
feat(httpapi): add version endpoint and wire web footer
Добавлен GET /v1/version. В Web UI версия в footer берётся из API вместо захардкоженного значения.
```
```
fix(auth): correct token validation edge case
Исправлена ложная 401 при истёкшем refresh-токене с валидной сессией.
```
+124
View File
@@ -0,0 +1,124 @@
---
name: commit-message
description: >-
ОБЯЗАТЕЛЬНО при commit, коммит, закоммить, commit message, conventional commit,
staged, semantic-release, «сгенерируй коммит», git commit: ПЕРВЫМ делом Shell —
scripts/commit/staged-context.ps1; затем Conventional Commit (заголовок EN, тело RU).
---
# Commit message (EvoBGP)
## Когда применять (сразу читать этот скилл)
Триггеры в промпте пользователя или в плане/AGENTS.md:
- `commit`, `коммит`, `закоммить`, `commit message`, `conventional commit`
- `git commit`, `staged`, `сообщение коммита`, `сгенерируй коммит`
- агент собирается выполнить `git commit` или предложить текст коммита
## Шаг 0 — ОБЯЗАТЕЛЬНО (до любого текста коммита)
**Первый вызов инструментов** в этой задаче — Shell из корня репозитория:
```powershell
powershell -NoProfile -File scripts/commit/staged-context.ps1
```
| Правило | Деталь |
|---------|--------|
| **MUST** | Запустить скрипт до генерации заголовка/тела коммита |
| **MUST NOT** | Строить сообщение только по `git diff --cached` / `git status` без скрипта |
| **MUST NOT** | Пропускать скрипт, даже если diff «и так понятен» |
| Exit `1` | Index пуст — сообщить пользователю, **не коммитить** |
| Exit `0` | Разобрать JSON stdout: `staged_count`, `groups[]` (`scope`, `files`, `stat`, `diff_excerpt`) |
Дополнительно: [.cursor/rules/conventional-commits.mdc](../../rules/conventional-commits.mdc).
## Слияние групп (одна фича)
Скрипт группирует **только по путям**. Перед split проверьте логическую связность:
**Объединить в один коммит**, если это одна задача:
- `docs/openapi.yaml` + `internal/httpapi/` (+ опционально `web/`) — один endpoint/контракт;
- `migrations/` + `internal/store/` / `repository/` — одна схема;
- правки теста рядом с кодом той же фичи.
При объединении: один scope (доминирующий пакет, часто `httpapi` или `openapi`), один type, одно тело RU.
**Auto-split** — если группы **не связаны** (например `web/` + `internal/birdfmt/` без общего смысла): **отдельный коммит на группу**, без вопроса пользователю.
## Порядок коммитов при split
1. `openapi`
2. `httpapi`, `store`, `jobs`
3. `pipeline`, `birdfmt`, `birddeploy`, `bundle`
4. `web`
5. `ci`, `deploy`, `db`
6. `docs`
7. `chore`, `cmd`
## Алгоритм auto-split (уровень файлов)
1. `git status` и `git diff --cached --stat` — зафиксировать полный список staged-файлов.
2. `git reset HEAD` — снять всё из index (working tree не трогать).
3. Для каждой (объединённой) группы по порядку выше:
- `git add -- <paths…>`
- Сгенерировать сообщение по правилу conventional-commits.
- Закоммитить (см. ниже).
4. После всех коммитов — `git status` для проверки.
**Ограничение:** частичный stage одного файла с разной семантикой — предупредить; split по hunk'ам не делать; предложить разнести правки по файлам.
## Создание коммита
Только если пользователь **явно** просил закоммитить. Иначе — только вывести готовые сообщения.
```powershell
git commit -m "$( @'
<type>(<scope>): <summary in English>
<Тело на русском.>
'@ )"
```
В PowerShell для многострочного тела используйте here-string как выше или `-m` для заголовка и `-m` для тела.
Перед коммитом: `git status`, `git diff --cached` для группы — убедиться, что stage соответствует сообщению.
## Генерация текста
По `groups[].diff_excerpt`, `stat`, `files`:
- **type** — по смыслу diff (`feat` / `fix` / …), не по умолчанию `chore`.
- **scope** — из JSON группы или доминирующий при merge.
- **summary** — конкретный, английский, императив.
- **body** — русский: что, зачем, edge cases, breaking impact.
## Вывод пользователю
Для **каждого** коммита:
### 1. Готовое сообщение
```
<type>(<scope>): <summary>
<тело>
```
### 2. Пояснение (RU)
- Почему выбран type/scope.
- Риски и impact.
- Split: сколько коммитов и почему.
## Не смешивать
Один commit message — одна primary intent. Не объединять несвязанный `fix` и `feat` в один заголовок.
## Ссылки
- Правило: `.cursor/rules/conventional-commits.mdc`
- Скрипт: `scripts/commit/staged-context.ps1`
- Просмотр групп: `powershell -File scripts/commit/staged-context.ps1 | ConvertFrom-Json`
+32 -42
View File
@@ -1,75 +1,65 @@
# Gitea Actions
В репозитории включён workflow [workflows/ci.yaml](workflows/ci.yaml).
Workflow-файлы в [workflows/](workflows/):
Job **changes** вычисляет два флага:
| Workflow | Триггер | Назначение |
|----------|---------|------------|
| [ci.yaml](workflows/ci.yaml) | push/PR в `main`/`master` | openapi, web, go, bird2, commitlint (PR) |
| [release.yaml](workflows/release.yaml) | после успешного CI (push в main) | semantic-release → tag, CHANGELOG, Gitea Release |
| [publish.yaml](workflows/publish.yaml) | push тега `v*` | docker buildx bake → Container Registry |
- **`openapi`** — в диффе есть `docs/openapi.yaml` или `redocly.yaml` → запускается **openapi** (Redocly lint).
- **`code`** — в диффе есть **хотя бы один файл вне «только документация»** → запускаются **go**, **bird2** и при push в main/master **docker-images**.
Подробнее о версионировании: [docs/releasing.md](../docs/releasing.md).
Если изменены **только** файлы из списка ниже, **`code=false`**: Go-тесты, bird2 и сборка образов **не** запускаются; при необходимости всё ещё выполняется **openapi**, если трогали OpenAPI или `redocly.yaml`.
## CI (quality gates)
**Список путей «только документация» (без полной сборки):** всё под `docs/`, корневой `README.md`, `web/README.md`, `.gitea/README.md`, `redocly.yaml`.
Job **changes** вычисляет флаги по путям в diff (`openapi`, `go`, `web`, `bird_conf`, …). Изменение `.gitea/workflows/*` поднимает полный прогон.
Любой другой путь (в т.ч. `.gitea/workflows/ci.yaml`, `go.mod`, `web/package.json`, `deploy/**`, `**/*.go`) включает полный пайплайн.
На **pull request** дополнительно запускается **commitlint** (Conventional Commits).
Требования к runner:
- Метка `ubuntu-latest` (или измените `runs-on` под свои labels).
- Job `bird2` ставит пакет **bird2** из репозитория **Ubuntu** на runner (как образ `evobgp-bird2` на `ubuntu:noble`) и вызывает `bird -c … -p` по файлам в checkout (без вложенного Docker: иначе bind-mount `/workspace/...` часто не виден демону Docker на хосте).
- Job **docker-images** требует **Docker** и (для push) доступ к Container Registry Gitea с runner’а.
- Метка `ubuntu-latest`.
- Job `bird2` ставит **bird2** из репозитория Ubuntu и вызывает `bird -c … -p`.
- Job **publish** требует Docker и доступ к Container Registry.
## Сборка и публикация образов (`docker-images`)
## Release (semantic-release)
Запускается только при **push** в ветки `main` или `master` (после успешного job `go`). Собираются образы из `deploy/docker/*` (все бинарники через `gobinary`, плюс `evobgp-web`, `evobgp-agent`, `evobgp-bird2`).
После успешного CI на push в `main`/`master` workflow **Release** запускает `npx semantic-release` (корневой `package.json`, [.releaserc.json](../.releaserc.json)).
### Секреты репозитория (для push в реестр)
Результат при releasable-коммитах:
Хост Container Registry в workflow **зашит**: **`git.shts.su`** (`docker/login-action` и теги образов).
- git tag `vX.Y.Z`;
- commit `CHANGELOG.md` с `[skip ci]`;
- Gitea Release с release notes.
В настройках репозитория Gitea: **Settings → Actions → Secrets** (или аналог):
### Секреты
| Секрет | Назначение |
|--------|------------|
| `ACTIONS_PAT` | (Опционально) PAT с правом **записи пакетов**. Если не задан, для `docker login` используется **`gitea.token`** (нужны права у workflow на пакеты). |
Один PAT — **`ACTIONS_PAT`**: push/t/tags, releases, запись в Container Registry. Если не задан — fallback на **`gitea.token`** (нужны права на releases и packages в Gitea).
Вход в реестр в workflow:
`GITEA_URL`: `https://git.shts.su` (задано в workflow).
- **registry:** `git.shts.su`;
- **username:** `${{ gitea.actor }}`;
- **password:** `${{ secrets.ACTIONS_PAT || gitea.token }}`.
## Publish (образы)
### Почему шаг «Log in to Gitea Registry» мог быть skipped
Запускается только при **push git-тега** `vX.Y.Z` (не при каждом push в main).
Раньше в `if:` стояло **`secrets.ACTIONS_PAT != ''`**. В **Gitea act_runner** такая проверка секрета в условии шага часто **всегда ложна**, из‑за этого логин **всегда skipped**. Сейчас условие только `docker_push == 'true'` (всегда при нормальном шаге **Prepare**), пароль: **`secrets.ACTIONS_PAT || gitea.token`**.
Собираются все образы из `deploy/docker/*` (`docker buildx bake default --push`). Semver из тега передаётся в Go-бинарники (`VERSION`, `GIT_SHA`, `BUILD_TIME`).
Если после правки `docker login` падает с 401/403 — выдайте job-токену или PAT право **записи в Container Registry** (в настройках Gitea / организации / репозитория, в зависимости от версии).
Registry: **`git.shts.su`**. Login: `${{ gitea.actor }}` / `${{ secrets.ACTIONS_PAT || gitea.token }}` (тот же **`ACTIONS_PAT`**, что и для release).
Если **`gitea.token`** в выражениях недоступен, в [ci.yaml](workflows/ci.yaml) замените пароль на `${{ secrets.ACTIONS_PAT || github.token }}`.
### Имена образов и теги
Формат:
### Теги образов
```text
git.shts.su/<owner_lowercase>/<имя>:<тег>
```
где `<имя>` — одно из: `evobgp-api`, `evobgp-all`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, `evobgp-node`, `evobgp-web`, `evobgp-web-all` (UI с прокси на `evobgp-all` для профиля Compose `microvps-full`), `evobgp-agent`, `evobgp-bird2`.
Имена: `evobgp-api`, `evobgp-all`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, `evobgp-node`, `evobgp-web`, `evobgp-web-all`, `evobgp-agent`, `evobgp-bird2`.
Теги: `latest`, короткий SHA (7 символов), `sha-<полный_commit_sha>`.
Теги при релизе `v1.2.3`: `latest`, `v1.2.3`, короткий SHA, `sha-<full_commit>`.
Пример pull:
Пример:
```bash
docker pull git.shts.su/myuser/evobgp-api:latest
docker pull git.shts.su/myuser/evobgp-api:v1.2.3
```
Полная таблица образов, тегов, ссылок на страницы пакетов в Gitea и минимальный `docker run` для сервера без сборки — в [docs/quickstart.md](../docs/quickstart.md) (раздел «Готовые образы без сборки»).
Убедитесь, что в **Gitea** включены пакеты (Container Registry) и у токена есть права на создание/обновление образов. Другой хост реестра — правьте **`registry:`** и префикс **`IMG=`** в [ci.yaml](workflows/ci.yaml).
### Примечания
- Сборка с платформой `linux/amd64`. При необходимости добавьте `linux/arm64` и multi-arch в `docker buildx build`.
- Для PR job **docker-images** не выполняется (только push в main/master), чтобы не публиковать образы из чужих форков без явной политики.
Подробности — [docs/quickstart.md](../docs/quickstart.md), [deploy/docker/README.md](../deploy/docker/README.md).
+11 -129
View File
@@ -208,138 +208,20 @@ jobs:
done
# ---------------------------------------------------------------------------
# Docker: все Go-образы одним buildx bake (один go mod download, одна компиляция cmd/*,
# birdc собирается один раз для api/all). Registry cache: evobgp-buildcache:go-buildcache.
# ---------------------------------------------------------------------------
docker-go:
needs: [changes, go]
if: >-
always() &&
needs.changes.result == 'success' &&
needs.go.result != 'failure' &&
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.go == 'true' || needs.changes.outputs.docker_go == 'true')
commitlint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push Go images (bake)
env:
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
IMAGE_TAG: latest
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
SHA_FULL: ${{ github.sha }}
CACHE_REF_GO: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:go-buildcache
CACHE_REF_WEB: ""
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: package-lock.json
- name: Lint commit messages
run: |
set -euxo pipefail
cd "${{ github.workspace }}/deploy/docker"
sh write-bake-override.sh
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
-f docker-bake.hcl -f docker-bake.override.hcl go-images --push
# ---------------------------------------------------------------------------
# Docker: Web (evobgp-web, evobgp-web-all) — один npm ci (кэш) + два nginx-тега.
# ---------------------------------------------------------------------------
docker-web:
needs: [changes]
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.docker_web == 'true')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push Web images (bake)
env:
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
IMAGE_TAG: latest
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
SHA_FULL: ${{ github.sha }}
CACHE_REF_GO: ""
CACHE_REF_WEB: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
run: |
set -euxo pipefail
cd "${{ github.workspace }}/deploy/docker"
sh write-bake-override.sh
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
-f docker-bake.hcl -f docker-bake.override.hcl web-images --push
# ---------------------------------------------------------------------------
# Docker: BIRD2 (evobgp-bird2).
# Собирается только при изменении deploy/bird/ или Dockerfile bird2.
# ---------------------------------------------------------------------------
docker-bird:
needs: [changes]
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.bird_conf == 'true' || needs.changes.outputs.docker_bird == 'true')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push evobgp-bird2 (bake)
env:
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
IMAGE_TAG: latest
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
SHA_FULL: ${{ github.sha }}
CACHE_REF_GO: ""
CACHE_REF_WEB: ""
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
run: |
set -euxo pipefail
cd "${{ github.workspace }}/deploy/docker"
sh write-bake-override.sh
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
-f docker-bake.hcl -f docker-bake.override.hcl evobgp-bird2 --push
npm ci
npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}"
+48
View File
@@ -0,0 +1,48 @@
name: Publish
on:
push:
tags: ["v*"]
jobs:
publish-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
ref="${GITHUB_REF_NAME:-}"
version="${ref#v}"
echo "version=$version" >> "$GITHUB_OUTPUT"
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push images (bake)
env:
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
IMAGE_TAG: latest
VERSION: ${{ steps.meta.outputs.version }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
SHA_FULL: ${{ github.sha }}
BUILD_TIME: ${{ steps.meta.outputs.build_time }}
CACHE_REF_GO: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:go-buildcache
CACHE_REF_WEB: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
run: |
set -euxo pipefail
cd "${{ github.workspace }}/deploy/docker"
sh write-bake-override.sh
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
-f docker-bake.hcl -f docker-bake.override.hcl default --push
+33
View File
@@ -0,0 +1,33 @@
name: Release
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main, master]
jobs:
release:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT || gitea.token }}
persist-credentials: true
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: package-lock.json
- name: Install release tooling
run: npm ci
- name: Semantic release
run: npx semantic-release
env:
GITEA_URL: https://git.shts.su
GITEA_TOKEN: ${{ secrets.ACTIONS_PAT || gitea.token }}
+14
View File
@@ -0,0 +1,14 @@
# Root npm (semantic-release, commitlint) — npm ci in CI, never commit deps
node_modules/
# Generated by deploy/docker/write-bake-override.sh (CI/local bake)
deploy/docker/docker-bake.override.hcl
# OS
.DS_Store
Thumbs.db
# Env / secrets (local only)
.env
.env.*
!.env.example
+49
View File
@@ -0,0 +1,49 @@
{
"branches": ["main", "master"],
"tagFormat": "v${version}",
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "breaking": true, "release": "major" }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@markwylde/semantic-release-gitea",
{
"giteaUrl": "https://git.shts.su",
"assets": [
{
"path": "CHANGELOG.md",
"label": "Changelog"
}
]
}
]
]
}
+9
View File
@@ -36,6 +36,15 @@
- **Повторное использование:** если [docs/architecture.md](docs/architecture.md) уже описывает поток — не пересказывайте его длинно; укажите документ и конкретный подпункт задачи.
- **Длинные планы:** `.cursor/plans/*.plan.md` — для истории решений; для навигации пользователю достаточно `docs/`; не читайте план целиком без причины.
## Коммиты (Conventional Commits)
Если пользователь просит **коммит**, **commit message**, **закоммить**, **git commit** или это следует из плана — **сразу**:
1. Shell: `powershell -NoProfile -File scripts/commit/staged-context.ps1` (первый вызов, до текста коммита).
2. Скилл [.cursor/skills/commit-message/SKILL.md](.cursor/skills/commit-message/SKILL.md) и правило [.cursor/rules/conventional-commits.mdc](.cursor/rules/conventional-commits.mdc).
Без вывода скрипта (exit 0) **не** придумывать сообщение коммита. Заголовок — EN, тело — RU; несвязанные области — auto-split (скилл).
## Команды и среда
- Консоль пользователя: **PowerShell**; пути в стиле `deploy\compose`.
+8
View File
@@ -44,4 +44,12 @@ docker compose --profile reference up -d
- **Go:** модуль `evobgp`, точки входа в `cmd/`.
- **Веб:** SvelteKit в каталоге `web/` (см. [web/README.md](web/README.md)).
## Версионирование и релизы
Версии определяются автоматически из Conventional Commits при merge в `main` (semantic-release на Gitea Actions). Первая версия — **1.0.0**; ручной bump не нужен.
- [docs/releasing.md](docs/releasing.md) — пайплайн, commit conventions, секреты CI
- API: `GET /version` и `GET /v1/version` (поле `version`)
- Docker-образы публикуются по git-тегу `vX.Y.Z` (workflow **Publish**)
Лицензия и условия использования — по политике владельца репозитория.
+2 -1
View File
@@ -19,6 +19,7 @@ import (
"evobgp/internal/platform"
"evobgp/internal/render"
"evobgp/internal/scheduler"
"evobgp/internal/version"
)
// microVPS entrypoint: один процесс — HTTP API и фоновые воркеры scheduler, ingest, render, deploy (общий store и jobs.Registry).
@@ -37,7 +38,7 @@ func main() {
log.Fatal(err)
}
defer srv.Close()
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
observability.SetBuildInfo(version.Version, version.GitSHA)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
+2 -1
View File
@@ -15,6 +15,7 @@ import (
"evobgp/internal/httpapi"
"evobgp/internal/observability"
"evobgp/internal/platform"
"evobgp/internal/version"
)
func main() {
@@ -34,7 +35,7 @@ func main() {
}
defer srv.Close()
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
observability.SetBuildInfo(version.Version, version.GitSHA)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
+7 -1
View File
@@ -2,7 +2,11 @@
## CI (Gitea Actions)
Сборка образов — **`docker buildx bake`** (`deploy/docker/docker-bake.hcl`), а не матрица из восьми отдельных `docker build`.
Сборка образов — **`docker buildx bake`** (`deploy/docker/docker-bake.hcl`), не отдельные `docker build`.
**Publish:** push git-тега `vX.Y.Z` запускает [.gitea/workflows/publish.yaml](../.gitea/workflows/publish.yaml) — образы с semver-тегом, `latest`, `sha-*`. Переменная `VERSION` из тега попадает в Go-бинарники через `-ldflags`.
Локально BuildKit также кэширует `/go/pkg/mod` и `~/.cache/go-build` через `RUN --mount=type=cache`.
| Было | Стало |
|------|--------|
@@ -31,6 +35,8 @@ cd deploy/docker
export REGISTRY=git.shts.su/<owner>
export IMAGE_TAG=latest
export SHORT_SHA=$(git rev-parse --short HEAD)
export VERSION=dev
export BUILD_TIME=
sh write-bake-override.sh
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl -f docker-bake.override.hcl go-images
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl -f docker-bake.override.hcl web-images
+24 -8
View File
@@ -19,6 +19,14 @@ variable "SHA_FULL" {
default = ""
}
variable "VERSION" {
default = "dev"
}
variable "BUILD_TIME" {
default = ""
}
variable "CACHE_REF_GO" {
default = ""
}
@@ -87,6 +95,11 @@ target "go-build-all" {
dockerfile = "deploy/docker/gobinary/Dockerfile"
target = "build-all"
platforms = ["linux/amd64"]
args = {
VERSION = VERSION
GIT_SHA = notequal("", SHA_FULL) ? SHA_FULL : SHORT_SHA
BUILD_TIME = BUILD_TIME
}
contexts = {
deps = "target:go-deps"
}
@@ -127,14 +140,17 @@ target "_go-runtime-birdc" {
function "image-tags" {
params = [name]
result = notequal("", SHA_FULL) ? [
"${REGISTRY}/${name}:${IMAGE_TAG}",
"${REGISTRY}/${name}:${SHORT_SHA}",
"${REGISTRY}/${name}:sha-${SHA_FULL}",
] : [
"${REGISTRY}/${name}:${IMAGE_TAG}",
"${REGISTRY}/${name}:${SHORT_SHA}",
]
result = concat(
notequal("", SHA_FULL) ? [
"${REGISTRY}/${name}:${IMAGE_TAG}",
"${REGISTRY}/${name}:${SHORT_SHA}",
"${REGISTRY}/${name}:sha-${SHA_FULL}",
] : [
"${REGISTRY}/${name}:${IMAGE_TAG}",
"${REGISTRY}/${name}:${SHORT_SHA}",
],
notequal(VERSION, "dev") && notequal(VERSION, "") ? ["${REGISTRY}/${name}:v${VERSION}"] : []
)
}
target "evobgp-api" {
+18 -2
View File
@@ -10,22 +10,38 @@ RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
FROM deps AS build-all
COPY . .
ARG VERSION=dev
ARG GIT_SHA=unknown
ARG BUILD_TIME=
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
set -eux; \
mkdir -p /out; \
for d in cmd/*/; do \
name="$(basename "$d")"; \
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "/out/${name}" "./cmd/${name}"; \
CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w \
-X evobgp/internal/version.Version=${VERSION} \
-X evobgp/internal/version.GitSHA=${GIT_SHA} \
-X evobgp/internal/version.BuildTime=${BUILD_TIME}" \
-o "/out/${name}" "./cmd/${name}"; \
done
# Один бинарь (локальная сборка); в CI — build-all + runtime.
FROM deps AS build
COPY . .
ARG BIN=evobgp-api
ARG VERSION=dev
ARG GIT_SHA=unknown
ARG BUILD_TIME=
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/evobgp "./cmd/${BIN}"
CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w \
-X evobgp/internal/version.Version=${VERSION} \
-X evobgp/internal/version.GitSHA=${GIT_SHA} \
-X evobgp/internal/version.BuildTime=${BUILD_TIME}" \
-o /out/evobgp "./cmd/${BIN}"
FROM public.ecr.aws/docker/library/debian:bookworm-slim AS birdc
ARG BIRD_VERSION=2.14
+8
View File
@@ -6,6 +6,8 @@ REGISTRY="${REGISTRY:?REGISTRY required}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
SHORT_SHA="${SHORT_SHA:-dev}"
SHA_FULL="${SHA_FULL:-}"
VERSION="${VERSION:-dev}"
BUILD_TIME="${BUILD_TIME:-}"
CACHE_REF_GO="${CACHE_REF_GO:-}"
CACHE_REF_WEB="${CACHE_REF_WEB:-}"
cat >"$OUT" <<EOF
@@ -22,6 +24,12 @@ variable "SHORT_SHA" {
variable "SHA_FULL" {
default = "${SHA_FULL}"
}
variable "VERSION" {
default = "${VERSION}"
}
variable "BUILD_TIME" {
default = "${BUILD_TIME}"
}
variable "CACHE_REF_GO" {
default = "${CACHE_REF_GO}"
}
+7
View File
@@ -20,6 +20,7 @@
| [api.md](api.md) | REST: префикс `/v1`, публичные маршруты, ссылки на OpenAPI |
| [router-lists-ui-integration.md](router-lists-ui-integration.md) | Интеграция `router-lists-ui` с EvoBGP API (`DOMAINS/IP_RANGES/AS_PREFIXES/communities`) |
| [access.md](access.md) | Выдача доступа: API-ключи, роли, нода, CORS |
| [releasing.md](releasing.md) | Автоматические релизы, Conventional Commits, CI |
| [openapi.yaml](openapi.yaml) | Источник правды по контракту API |
| [OPENAPI-GITEA.md](OPENAPI-GITEA.md) | Как открыть HTML-документацию API (в т.ч. из Gitea) |
| [evobgp-api-sketches.md](evobgp-api-sketches.md) | Ранний черновик идей API (контекст, не замена OpenAPI) |
@@ -31,7 +32,13 @@
| [../.cursor/rules/engineering.mdc](../.cursor/rules/engineering.mdc) | Go, API, migrations, security, enforcement |
| [../.cursor/rules/web-shadcn.mdc](../.cursor/rules/web-shadcn.mdc) | SvelteKit, shadcn-svelte |
| [../.cursor/rules/networking-bird.mdc](../.cursor/rules/networking-bird.mdc) | BIRD2, BGP policy, IP/CIDR |
| [../.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc) | Conventional Commits (заголовок EN, тело RU) |
### Сообщения коммитов (Cursor)
После `git add` попросите агента: **«сгенерируй коммит по staged»**, **«закоммить»**, **«commit message»** — агент **обязан первым делом** запустить `scripts/commit/staged-context.ps1`, затем скилл [commit-message](../.cursor/skills/commit-message/SKILL.md) (заголовок EN, тело RU, auto-split). Просмотр групп вручную: `powershell -NoProfile -File scripts/commit/staged-context.ps1 | ConvertFrom-Json`.
## Репозиторий и CI
- [../.gitea/README.md](../.gitea/README.md) — Gitea Actions, runner, сборка образов и push в Container Registry.
- [releasing.md](releasing.md) — semantic-release, commit conventions, теги образов.
+30 -2
View File
@@ -1,10 +1,12 @@
openapi: 3.1.0
info:
title: EvoBGP Control Plane API
version: 0.1.0
version: 1.0.0
description: |
REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs).
**Актуальная semver-сборка:** `GET /version` или `GET /v1/version` (поле `version`; совпадает с git-тегом `vX.Y.Z`).
**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.
Ошибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).
Пагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.
@@ -758,8 +760,14 @@ components:
VersionInfo:
type: object
properties:
version:
type: string
description: Semver сборки (git tag без префикса v).
example: "1.2.3"
api_version:
type: string
deprecated: true
description: Alias поля `version` (сохранён для обратной совместимости).
git_sha:
type: string
build_time:
@@ -967,11 +975,31 @@ paths:
default:
$ref: "#/components/responses/DefaultProblem"
/version:
get:
tags: [System]
summary: Версия сборки (корневой путь)
description: |
Аналог `GET /v1/version`. Публичный маршрут без аутентификации.
Semver в поле `version` задаётся при сборке Docker-образов из git-тега.
operationId: getVersionRoot
responses:
"200":
description: Метаданные сборки.
content:
application/json:
schema:
$ref: "#/components/schemas/VersionInfo"
default:
$ref: "#/components/responses/DefaultProblem"
/v1/version:
get:
tags: [System]
summary: Версия сборки
description: Версия API и control-plane (`git_sha`, `build_time` и др.).
description: |
Semver control-plane и метаданные сборки (`git_sha`, `build_time`).
Дублирует `GET /version`.
operationId: getVersion
responses:
"200":
+83
View File
@@ -0,0 +1,83 @@
# Релизы и версионирование EvoBGP
EvoBGP использует [Conventional Commits](https://www.conventionalcommits.org/) и [semantic-release](https://semantic-release.gitbook.io/) для полностью автоматических релизов на Gitea (`git.shts.su`). Ручное повышение версии в коде не требуется.
## Как определяется версия
| Тип коммита | Bump |
|-------------|------|
| `feat` | minor (1.0.0 → 1.1.0) |
| `fix`, `perf` | patch (1.0.0 → 1.0.1) |
| `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) |
| `docs`, `chore`, `ci`, `test`, `refactor` | без релиза |
Первый релиз при отсутствии git-тегов — **1.0.0**, если есть releasable-коммиты.
Подробные правила сообщений коммитов: [.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc).
## CI-пайплайн
```text
push/merge в main
→ CI (openapi, web, go, bird2, commitlint на PR)
→ Release (semantic-release после успешного CI)
→ git tag vX.Y.Z
→ CHANGELOG.md + commit [skip ci]
→ Gitea Release с notes
→ Publish (push тега v*)
→ docker buildx bake с VERSION из тега
→ образы: latest, vX.Y.Z, sha-*, короткий SHA
```
Workflow-файлы:
- [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml) — quality gates
- [.gitea/workflows/release.yaml](../.gitea/workflows/release.yaml) — semantic-release
- [.gitea/workflows/publish.yaml](../.gitea/workflows/publish.yaml) — публикация образов
Конфиг semantic-release: [.releaserc.json](../.releaserc.json).
## Секреты Gitea
Один PAT в репозитории — **`ACTIONS_PAT`** (Settings → Actions → Secrets). Используется для semantic-release, push тегов/CHANGELOG и docker login в registry.
| Право PAT | Зачем |
|-----------|--------|
| push / write repository | commit `CHANGELOG.md`, push тегов |
| releases | Gitea Release через semantic-release |
| packages (Container Registry) | workflow **Publish** |
Если `ACTIONS_PAT` не задан, workflow пробует **`gitea.token`** job-токен (нужны права на releases и packages в настройках Gitea).
## Источник правды для версии в runtime
Semver из git-тега пробрасывается в Go-бинарники через `-ldflags` при сборке Docker (`deploy/docker/gobinary/Dockerfile`). Пакет [`internal/version`](../internal/version/version.go):
- локально (`go run`) — `version: "dev"`
- в образе после релиза — совпадает с тегом (например `1.2.3`)
API:
- `GET /version`
- `GET /v1/version`
Ответ включает `version`, `git_sha`, `build_time`; поле `api_version` — deprecated alias.
Web UI показывает версию из API (footer sidebar, страница «Мониторинг»).
## CHANGELOG
Файл [`CHANGELOG.md`](../CHANGELOG.md) создаётся и обновляется semantic-release. Копия прикрепляется к Gitea Release.
## Проверка после релиза
1. В Gitea: тег `vX.Y.Z` и Release с notes.
2. Container Registry: образы с тегом `vX.Y.Z`.
3. `curl http://localhost:8080/version` → `"version":"X.Y.Z"`.
4. Footer Web UI → `vX.Y.Z`.
## Первый релиз (bootstrap)
Merge PR в `main` с conventional commit типа `feat(release): ...` (не `chore` — иначе релиз не создастся). Ожидаемый результат: **v1.0.0**.
После merge убедитесь, что workflow **Release** завершился успешно и workflow **Publish** собрал образы по тегу.
+11 -4
View File
@@ -21,6 +21,7 @@ import (
"evobgp/internal/pipeline"
"evobgp/internal/reports"
"evobgp/internal/store"
"evobgp/internal/version"
)
// Handler returns the root HTTP handler (system routes public; rest under /v1/ authenticated).
@@ -30,6 +31,7 @@ func (s *Server) Handler() http.Handler {
wrappedV1 := http.StripPrefix("/v1", v1)
s.mux.Handle("GET /metrics", observability.MetricsHandler())
s.mux.HandleFunc("GET /version", s.handleVersion)
s.mux.HandleFunc("GET /v1/health", s.handleHealth)
s.mux.HandleFunc("GET /v1/ready", s.handleReady)
s.mux.HandleFunc("GET /v1/version", s.handleVersion)
@@ -96,11 +98,16 @@ func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
sha := strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA"))
if sha == "" {
sha = "unknown"
ver, sha, buildTime := version.Info()
resp := map[string]string{
"version": ver,
"api_version": ver,
"git_sha": sha,
}
writeJSON(w, http.StatusOK, map[string]string{"api_version": "0.1.0", "git_sha": sha})
if buildTime != "" {
resp["build_time"] = buildTime
}
writeJSON(w, http.StatusOK, resp)
}
func moduleJSON(mod *store.Module) map[string]any {
+58
View File
@@ -13,6 +13,7 @@ import (
"evobgp/internal/jobs"
"evobgp/internal/signing"
"evobgp/internal/version"
)
const testBundleSeed = "0101010101010101010101010101010101010101010101010101010101010101"
@@ -371,3 +372,60 @@ evobgp_p_01234567 BGP master start 2026-04-09 Connect
t.Fatalf("expected protocol state Connect, got %q", got["evobgp_p_01234567"])
}
}
func TestVersionEndpoints(t *testing.T) {
srv, err := New(Options{
InsecureDev: true,
SeedDemo: true,
BundleSeedHex: testBundleSeed,
})
if err != nil {
t.Fatal(err)
}
defer srv.Close()
oldVer := version.Version
oldSHA := version.GitSHA
oldTime := version.BuildTime
version.Version = "1.2.3"
version.GitSHA = "abc123def456"
version.BuildTime = "2026-05-20T12:00:00Z"
t.Cleanup(func() {
version.Version = oldVer
version.GitSHA = oldSHA
version.BuildTime = oldTime
})
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
for _, path := range []string{"/version", "/v1/version"} {
t.Run(path, func(t *testing.T) {
resp, err := http.Get(ts.URL + path)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d: %s", resp.StatusCode, b)
}
var body map[string]string
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body["version"] != "1.2.3" {
t.Fatalf("version=%q want 1.2.3", body["version"])
}
if body["api_version"] != "1.2.3" {
t.Fatalf("api_version=%q want 1.2.3", body["api_version"])
}
if body["git_sha"] != "abc123def456" {
t.Fatalf("git_sha=%q", body["git_sha"])
}
if body["build_time"] != "2026-05-20T12:00:00Z" {
t.Fatalf("build_time=%q", body["build_time"])
}
})
}
}
+23
View File
@@ -0,0 +1,23 @@
// Package version holds build metadata injected at link time via -ldflags.
package version
// Link-time variables (defaults for local development).
var (
Version = "dev"
GitSHA = "unknown"
BuildTime = ""
)
// Info returns build metadata, substituting safe defaults for empty values.
func Info() (ver, gitSHA, buildTime string) {
ver = Version
if ver == "" {
ver = "dev"
}
gitSHA = GitSHA
if gitSHA == "" {
gitSHA = "unknown"
}
buildTime = BuildTime
return ver, gitSHA, buildTime
}
+7691
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
{
"name": "evobgp-release",
"private": true,
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@markwylde/semantic-release-gitea": "^2.2.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"semantic-release": "^25.0.2"
}
}
+181
View File
@@ -0,0 +1,181 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Staged git changes grouped by EvoBGP scope for Conventional Commit generation.
.DESCRIPTION
Outputs JSON to stdout: file groups, --stat, and truncated patch per group.
Exit 1 if nothing is staged. Does not write commit messages.
.EXAMPLE
powershell -NoProfile -File scripts/commit/staged-context.ps1
powershell -NoProfile -File scripts/commit/staged-context.ps1 | ConvertFrom-Json
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$MaxLinesPerGroup = 120
$MaxLinesTotal = 500
function Test-GitRepository {
$null = git rev-parse --git-dir 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Error 'staged-context: not a git repository'
exit 2
}
}
function Get-ScopeForPath {
param([string]$Path)
$p = $Path -replace '\\', '/'
# Longest / most specific prefixes first
$rules = @(
@{ Prefix = 'docs/openapi.yaml'; Scope = 'openapi' }
@{ Prefix = 'redocly.yaml'; Scope = 'openapi' }
@{ Prefix = 'internal/httpapi/'; Scope = 'httpapi' }
@{ Prefix = 'internal/store/'; Scope = 'store' }
@{ Prefix = 'internal/repository/'; Scope = 'store' }
@{ Prefix = 'internal/db/'; Scope = 'store' }
@{ Prefix = 'internal/jobs/'; Scope = 'jobs' }
@{ Prefix = 'internal/pipeline/'; Scope = 'pipeline' }
@{ Prefix = 'internal/birdfmt/'; Scope = 'birdfmt' }
@{ Prefix = 'internal/birddeploy/'; Scope = 'birddeploy' }
@{ Prefix = 'internal/bundle/'; Scope = 'bundle' }
@{ Prefix = 'internal/signing/'; Scope = 'bundle' }
@{ Prefix = 'cmd/'; Scope = 'cmd' }
@{ Prefix = 'web/'; Scope = 'web' }
@{ Prefix = 'migrations/'; Scope = 'db' }
@{ Prefix = 'docs/'; Scope = 'docs' }
@{ Prefix = '.gitea/'; Scope = 'ci' }
@{ Prefix = 'deploy/'; Scope = 'deploy' }
@{ Prefix = '.cursor/'; Scope = 'chore' }
)
foreach ($rule in $rules) {
if ($p -eq $rule.Prefix.TrimEnd('/') -or $p.StartsWith($rule.Prefix)) {
return $rule.Scope
}
}
return 'chore'
}
function Get-ScopeSortOrder {
param([string]$Scope)
$order = @{
openapi = 10
httpapi = 20
store = 21
jobs = 22
pipeline = 30
birdfmt = 31
birddeploy = 32
bundle = 33
web = 40
ci = 50
deploy = 51
db = 52
docs = 60
cmd = 70
chore = 80
}
if ($order.ContainsKey($Scope)) { return $order[$Scope] }
return 99
}
function Invoke-Git {
param([string[]]$GitArgs)
$out = & git @GitArgs 2>&1
if ($LASTEXITCODE -ne 0) {
$msg = ($out | Out-String).Trim()
throw "git $($GitArgs -join ' ') failed: $msg"
}
return ($out | Out-String).TrimEnd()
}
function New-GitArgs {
param([string[]]$Base, [string[]]$Paths)
if ($Paths.Count -eq 0) { return $Base }
return $Base + '--' + $Paths
}
function Get-TruncatedDiff {
param(
[string[]]$Files,
[int]$MaxLines,
[ref]$TotalLinesUsed
)
if ($Files.Count -eq 0) { return '' }
$remaining = $MaxLinesTotal - $TotalLinesUsed.Value
if ($remaining -le 0) {
return '[diff truncated: global line budget exceeded]'
}
$cap = [Math]::Min($MaxLines, $remaining)
$diff = Invoke-Git -GitArgs (New-GitArgs -Base @(
'diff', '--cached', '--no-color', '--unified=3'
) -Paths $Files)
if ([string]::IsNullOrWhiteSpace($diff)) { return '' }
$lines = $diff -split "`n", -1
if ($lines.Count -le $cap) {
$TotalLinesUsed.Value += $lines.Count
return $diff
}
$truncated = ($lines[0..($cap - 1)] -join "`n") + "`n... [truncated: $($lines.Count - $cap) more lines]"
$TotalLinesUsed.Value += $cap
return $truncated
}
Test-GitRepository
$stagedRaw = Invoke-Git -GitArgs @('diff', '--cached', '--name-only')
if ([string]::IsNullOrWhiteSpace($stagedRaw)) {
[Console]::Error.WriteLine('staged-context: no staged changes (git index is empty)')
exit 1
}
$stagedFiles = @(
$stagedRaw -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
)
$stagedFiles = $stagedFiles | Sort-Object -Unique
$scopeBuckets = @{}
foreach ($f in $stagedFiles) {
$scope = Get-ScopeForPath -Path $f
if (-not $scopeBuckets.ContainsKey($scope)) {
$scopeBuckets[$scope] = [System.Collections.Generic.List[string]]::new()
}
$scopeBuckets[$scope].Add($f) | Out-Null
}
$totalLinesUsed = 0
$groups = New-Object System.Collections.Generic.List[object]
$scopeKeys = @($scopeBuckets.Keys | Sort-Object { Get-ScopeSortOrder $_ })
foreach ($scope in $scopeKeys) {
$files = @($scopeBuckets[$scope] | Sort-Object)
$stat = Invoke-Git -GitArgs (New-GitArgs -Base @('diff', '--cached', '--stat') -Paths $files)
$diffExcerpt = Get-TruncatedDiff -Files $files -MaxLines $MaxLinesPerGroup -TotalLinesUsed ([ref]$totalLinesUsed)
$groups.Add([ordered]@{
scope = $scope
files = $files
stat = $stat
diff_excerpt = $diffExcerpt
}) | Out-Null
}
$result = [ordered]@{
staged_count = $stagedFiles.Count
groups = $groups.ToArray()
}
$json = $result | ConvertTo-Json -Depth 6 -Compress:$false
# UTF-8 stdout for agents / ConvertFrom-Json
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
Write-Output $json
exit 0
@@ -7,6 +7,7 @@
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
import AppNavLinks from './app-nav-links.svelte';
import AppVersion from './app-version.svelte';
import ThemeMenu from './theme-menu.svelte';
let {
@@ -54,4 +55,5 @@
</div>
<Separator />
<AppNavLinks {collapsed} />
<AppVersion />
</aside>
@@ -0,0 +1,30 @@
<script lang="ts">
import { onMount } from 'svelte';
import { apiJSON } from '$lib/api/client.js';
type VersionInfo = {
version?: string;
api_version?: string;
};
let label = $state<string | null>(null);
onMount(() => {
void (async () => {
try {
const info = await apiJSON<VersionInfo>('/v1/version');
const ver =
(typeof info.version === 'string' && info.version) ||
(typeof info.api_version === 'string' && info.api_version) ||
'';
label = ver ? `v${ver}` : null;
} catch {
label = null;
}
})();
});
</script>
{#if label}
<p class="truncate px-3 pb-3 text-xs text-sidebar-foreground/45" title={label}>{label}</p>
{/if}
+5
View File
@@ -100,8 +100,13 @@
const versionText = $derived.by(() => {
if (!version) return '—';
const ver =
(typeof version.version === 'string' && version.version) ||
(typeof version.api_version === 'string' && version.api_version) ||
'';
const sha =
typeof version.git_sha === 'string' && version.git_sha ? version.git_sha : 'unknown';
if (ver) return `${ver} (${sha.slice(0, 12)})`;
return sha.slice(0, 12);
});