feat: implement versioning and release management in the project
- 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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["@commitlint/config-conventional"]
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ alwaysApply: false
|
|||||||
|
|
||||||
# Conventional Commits (EvoBGP)
|
# Conventional Commits (EvoBGP)
|
||||||
|
|
||||||
|
Полное описание релизного пайплайна: [docs/releasing.md](../../docs/releasing.md).
|
||||||
|
|
||||||
## Триггеры (применить правило + скилл)
|
## Триггеры (применить правило + скилл)
|
||||||
|
|
||||||
Любой запрос на коммит или сообщение коммита: `commit`, `коммит`, `закоммить`, `git commit`, `commit message`, `conventional commit`, `staged`, «сгенерируй коммит» — в т.ч. если это указано в плане или [AGENTS.md](../../AGENTS.md).
|
Любой запрос на коммит или сообщение коммита: `commit`, `коммит`, `закоммить`, `git commit`, `commit message`, `conventional commit`, `staged`, «сгенерируй коммит» — в т.ч. если это указано в плане или [AGENTS.md](../../AGENTS.md).
|
||||||
|
|||||||
+32
-42
@@ -1,75 +1,65 @@
|
|||||||
# Gitea Actions
|
# 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).
|
Подробнее о версионировании: [docs/releasing.md](../docs/releasing.md).
|
||||||
- **`code`** — в диффе есть **хотя бы один файл вне «только документация»** → запускаются **go**, **bird2** и при push в main/master **docker-images**.
|
|
||||||
|
|
||||||
Если изменены **только** файлы из списка ниже, **`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:
|
Требования к runner:
|
||||||
|
|
||||||
- Метка `ubuntu-latest` (или измените `runs-on` под свои labels).
|
- Метка `ubuntu-latest`.
|
||||||
- Job `bird2` ставит пакет **bird2** из репозитория **Ubuntu** на runner (как образ `evobgp-bird2` на `ubuntu:noble`) и вызывает `bird -c … -p` по файлам в checkout (без вложенного Docker: иначе bind-mount `/workspace/...` часто не виден демону Docker на хосте).
|
- Job `bird2` ставит **bird2** из репозитория Ubuntu и вызывает `bird -c … -p`.
|
||||||
- Job **docker-images** требует **Docker** и (для push) доступ к Container Registry Gitea с runner’а.
|
- 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** (или аналог):
|
### Секреты
|
||||||
|
|
||||||
| Секрет | Назначение |
|
Один PAT — **`ACTIONS_PAT`**: push/t/tags, releases, запись в Container Registry. Если не задан — fallback на **`gitea.token`** (нужны права на releases и packages в Gitea).
|
||||||
|--------|------------|
|
|
||||||
| `ACTIONS_PAT` | (Опционально) PAT с правом **записи пакетов**. Если не задан, для `docker login` используется **`gitea.token`** (нужны права у workflow на пакеты). |
|
|
||||||
|
|
||||||
Вход в реестр в workflow:
|
`GITEA_URL`: `https://git.shts.su` (задано в workflow).
|
||||||
|
|
||||||
- **registry:** `git.shts.su`;
|
## Publish (образы)
|
||||||
- **username:** `${{ gitea.actor }}`;
|
|
||||||
- **password:** `${{ secrets.ACTIONS_PAT || gitea.token }}`.
|
|
||||||
|
|
||||||
### Почему шаг «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
|
```text
|
||||||
git.shts.su/<owner_lowercase>/<имя>:<тег>
|
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
|
```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) (раздел «Готовые образы без сборки»).
|
Подробности — [docs/quickstart.md](../docs/quickstart.md), [deploy/docker/README.md](../deploy/docker/README.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), чтобы не публиковать образы из чужих форков без явной политики.
|
|
||||||
|
|||||||
+11
-129
@@ -208,138 +208,20 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Docker: все Go-образы одним buildx bake (один go mod download, одна компиляция cmd/*,
|
commitlint:
|
||||||
# birdc собирается один раз для api/all). Registry cache: evobgp-buildcache:go-buildcache.
|
if: github.event_name == 'pull_request'
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
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')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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:
|
with:
|
||||||
registry: git.shts.su
|
fetch-depth: 0
|
||||||
username: ${{ gitea.actor }}
|
- uses: actions/setup-node@v4
|
||||||
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
with:
|
||||||
- name: Build and push Go images (bake)
|
node-version: "22"
|
||||||
env:
|
cache: npm
|
||||||
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
|
cache-dependency-path: package-lock.json
|
||||||
IMAGE_TAG: latest
|
- name: Lint commit messages
|
||||||
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"
|
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
cd "${{ github.workspace }}/deploy/docker"
|
npm ci
|
||||||
sh write-bake-override.sh
|
npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}"
|
||||||
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
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -44,4 +44,12 @@ docker compose --profile reference up -d
|
|||||||
- **Go:** модуль `evobgp`, точки входа в `cmd/`.
|
- **Go:** модуль `evobgp`, точки входа в `cmd/`.
|
||||||
- **Веб:** SvelteKit в каталоге `web/` (см. [web/README.md](web/README.md)).
|
- **Веб:** 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**)
|
||||||
|
|
||||||
Лицензия и условия использования — по политике владельца репозитория.
|
Лицензия и условия использования — по политике владельца репозитория.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"evobgp/internal/platform"
|
"evobgp/internal/platform"
|
||||||
"evobgp/internal/render"
|
"evobgp/internal/render"
|
||||||
"evobgp/internal/scheduler"
|
"evobgp/internal/scheduler"
|
||||||
|
"evobgp/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// microVPS entrypoint: один процесс — HTTP API и фоновые воркеры scheduler, ingest, render, deploy (общий store и jobs.Registry).
|
// microVPS entrypoint: один процесс — HTTP API и фоновые воркеры scheduler, ingest, render, deploy (общий store и jobs.Registry).
|
||||||
@@ -37,7 +38,7 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer srv.Close()
|
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)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"evobgp/internal/httpapi"
|
"evobgp/internal/httpapi"
|
||||||
"evobgp/internal/observability"
|
"evobgp/internal/observability"
|
||||||
"evobgp/internal/platform"
|
"evobgp/internal/platform"
|
||||||
|
"evobgp/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,7 +35,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer srv.Close()
|
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)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
## CI (Gitea Actions)
|
## 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 REGISTRY=git.shts.su/<owner>
|
||||||
export IMAGE_TAG=latest
|
export IMAGE_TAG=latest
|
||||||
export SHORT_SHA=$(git rev-parse --short HEAD)
|
export SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
export VERSION=dev
|
||||||
|
export BUILD_TIME=
|
||||||
sh write-bake-override.sh
|
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 go-images
|
||||||
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl -f docker-bake.override.hcl web-images
|
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl -f docker-bake.override.hcl web-images
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ variable "SHA_FULL" {
|
|||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "VERSION" {
|
||||||
|
default = "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "BUILD_TIME" {
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
variable "CACHE_REF_GO" {
|
variable "CACHE_REF_GO" {
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
@@ -87,6 +95,11 @@ target "go-build-all" {
|
|||||||
dockerfile = "deploy/docker/gobinary/Dockerfile"
|
dockerfile = "deploy/docker/gobinary/Dockerfile"
|
||||||
target = "build-all"
|
target = "build-all"
|
||||||
platforms = ["linux/amd64"]
|
platforms = ["linux/amd64"]
|
||||||
|
args = {
|
||||||
|
VERSION = VERSION
|
||||||
|
GIT_SHA = notequal("", SHA_FULL) ? SHA_FULL : SHORT_SHA
|
||||||
|
BUILD_TIME = BUILD_TIME
|
||||||
|
}
|
||||||
contexts = {
|
contexts = {
|
||||||
deps = "target:go-deps"
|
deps = "target:go-deps"
|
||||||
}
|
}
|
||||||
@@ -127,14 +140,17 @@ target "_go-runtime-birdc" {
|
|||||||
|
|
||||||
function "image-tags" {
|
function "image-tags" {
|
||||||
params = [name]
|
params = [name]
|
||||||
result = notequal("", SHA_FULL) ? [
|
result = concat(
|
||||||
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
notequal("", SHA_FULL) ? [
|
||||||
"${REGISTRY}/${name}:${SHORT_SHA}",
|
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
||||||
"${REGISTRY}/${name}:sha-${SHA_FULL}",
|
"${REGISTRY}/${name}:${SHORT_SHA}",
|
||||||
] : [
|
"${REGISTRY}/${name}:sha-${SHA_FULL}",
|
||||||
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
] : [
|
||||||
"${REGISTRY}/${name}:${SHORT_SHA}",
|
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
||||||
]
|
"${REGISTRY}/${name}:${SHORT_SHA}",
|
||||||
|
],
|
||||||
|
notequal(VERSION, "dev") && notequal(VERSION, "") ? ["${REGISTRY}/${name}:v${VERSION}"] : []
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
target "evobgp-api" {
|
target "evobgp-api" {
|
||||||
|
|||||||
@@ -10,22 +10,38 @@ RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
|||||||
|
|
||||||
FROM deps AS build-all
|
FROM deps AS build-all
|
||||||
COPY . .
|
COPY . .
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG GIT_SHA=unknown
|
||||||
|
ARG BUILD_TIME=
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||||
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
|
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
|
||||||
set -eux; \
|
set -eux; \
|
||||||
mkdir -p /out; \
|
mkdir -p /out; \
|
||||||
for d in cmd/*/; do \
|
for d in cmd/*/; do \
|
||||||
name="$(basename "$d")"; \
|
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
|
done
|
||||||
|
|
||||||
# Один бинарь (локальная сборка); в CI — build-all + runtime.
|
# Один бинарь (локальная сборка); в CI — build-all + runtime.
|
||||||
FROM deps AS build
|
FROM deps AS build
|
||||||
COPY . .
|
COPY . .
|
||||||
ARG BIN=evobgp-api
|
ARG BIN=evobgp-api
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG GIT_SHA=unknown
|
||||||
|
ARG BUILD_TIME=
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||||
--mount=type=cache,target=/root/.cache/go-build,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
|
FROM public.ecr.aws/docker/library/debian:bookworm-slim AS birdc
|
||||||
ARG BIRD_VERSION=2.14
|
ARG BIRD_VERSION=2.14
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ REGISTRY="${REGISTRY:?REGISTRY required}"
|
|||||||
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||||
SHORT_SHA="${SHORT_SHA:-dev}"
|
SHORT_SHA="${SHORT_SHA:-dev}"
|
||||||
SHA_FULL="${SHA_FULL:-}"
|
SHA_FULL="${SHA_FULL:-}"
|
||||||
|
VERSION="${VERSION:-dev}"
|
||||||
|
BUILD_TIME="${BUILD_TIME:-}"
|
||||||
CACHE_REF_GO="${CACHE_REF_GO:-}"
|
CACHE_REF_GO="${CACHE_REF_GO:-}"
|
||||||
CACHE_REF_WEB="${CACHE_REF_WEB:-}"
|
CACHE_REF_WEB="${CACHE_REF_WEB:-}"
|
||||||
cat >"$OUT" <<EOF
|
cat >"$OUT" <<EOF
|
||||||
@@ -22,6 +24,12 @@ variable "SHORT_SHA" {
|
|||||||
variable "SHA_FULL" {
|
variable "SHA_FULL" {
|
||||||
default = "${SHA_FULL}"
|
default = "${SHA_FULL}"
|
||||||
}
|
}
|
||||||
|
variable "VERSION" {
|
||||||
|
default = "${VERSION}"
|
||||||
|
}
|
||||||
|
variable "BUILD_TIME" {
|
||||||
|
default = "${BUILD_TIME}"
|
||||||
|
}
|
||||||
variable "CACHE_REF_GO" {
|
variable "CACHE_REF_GO" {
|
||||||
default = "${CACHE_REF_GO}"
|
default = "${CACHE_REF_GO}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
| [api.md](api.md) | REST: префикс `/v1`, публичные маршруты, ссылки на OpenAPI |
|
| [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`) |
|
| [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 |
|
| [access.md](access.md) | Выдача доступа: API-ключи, роли, нода, CORS |
|
||||||
|
| [releasing.md](releasing.md) | Автоматические релизы, Conventional Commits, CI |
|
||||||
| [openapi.yaml](openapi.yaml) | Источник правды по контракту API |
|
| [openapi.yaml](openapi.yaml) | Источник правды по контракту API |
|
||||||
| [OPENAPI-GITEA.md](OPENAPI-GITEA.md) | Как открыть HTML-документацию API (в т.ч. из Gitea) |
|
| [OPENAPI-GITEA.md](OPENAPI-GITEA.md) | Как открыть HTML-документацию API (в т.ч. из Gitea) |
|
||||||
| [evobgp-api-sketches.md](evobgp-api-sketches.md) | Ранний черновик идей API (контекст, не замена OpenAPI) |
|
| [evobgp-api-sketches.md](evobgp-api-sketches.md) | Ранний черновик идей API (контекст, не замена OpenAPI) |
|
||||||
@@ -40,3 +41,4 @@
|
|||||||
## Репозиторий и CI
|
## Репозиторий и CI
|
||||||
|
|
||||||
- [../.gitea/README.md](../.gitea/README.md) — Gitea Actions, runner, сборка образов и push в Container Registry.
|
- [../.gitea/README.md](../.gitea/README.md) — Gitea Actions, runner, сборка образов и push в Container Registry.
|
||||||
|
- [releasing.md](releasing.md) — semantic-release, commit conventions, теги образов.
|
||||||
|
|||||||
+30
-2
@@ -1,10 +1,12 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: EvoBGP Control Plane API
|
title: EvoBGP Control Plane API
|
||||||
version: 0.1.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs).
|
REST API управления префиксами, модулями ingest, ревизиями конфигурации BIRD и задачами (async jobs).
|
||||||
|
|
||||||
|
**Актуальная semver-сборка:** `GET /version` или `GET /v1/version` (поле `version`; совпадает с git-тегом `vX.Y.Z`).
|
||||||
|
|
||||||
**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.
|
**Соглашения:** префикс `/v1`; идентификаторы - UUID v7 или ULID (строки); время - ISO 8601 UTC.
|
||||||
Ошибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).
|
Ошибки - `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)).
|
||||||
Пагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.
|
Пагинация списков - `cursor` + `limit`; ответ содержит `items`, `next_cursor`, `has_more`.
|
||||||
@@ -758,8 +760,14 @@ components:
|
|||||||
VersionInfo:
|
VersionInfo:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: Semver сборки (git tag без префикса v).
|
||||||
|
example: "1.2.3"
|
||||||
api_version:
|
api_version:
|
||||||
type: string
|
type: string
|
||||||
|
deprecated: true
|
||||||
|
description: Alias поля `version` (сохранён для обратной совместимости).
|
||||||
git_sha:
|
git_sha:
|
||||||
type: string
|
type: string
|
||||||
build_time:
|
build_time:
|
||||||
@@ -967,11 +975,31 @@ paths:
|
|||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/DefaultProblem"
|
$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:
|
/v1/version:
|
||||||
get:
|
get:
|
||||||
tags: [System]
|
tags: [System]
|
||||||
summary: Версия сборки
|
summary: Версия сборки
|
||||||
description: Версия API и control-plane (`git_sha`, `build_time` и др.).
|
description: |
|
||||||
|
Semver control-plane и метаданные сборки (`git_sha`, `build_time`).
|
||||||
|
Дублирует `GET /version`.
|
||||||
operationId: getVersion
|
operationId: getVersion
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
|||||||
@@ -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** собрал образы по тегу.
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"evobgp/internal/pipeline"
|
"evobgp/internal/pipeline"
|
||||||
"evobgp/internal/reports"
|
"evobgp/internal/reports"
|
||||||
"evobgp/internal/store"
|
"evobgp/internal/store"
|
||||||
|
"evobgp/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler returns the root HTTP handler (system routes public; rest under /v1/ authenticated).
|
// 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)
|
wrappedV1 := http.StripPrefix("/v1", v1)
|
||||||
|
|
||||||
s.mux.Handle("GET /metrics", observability.MetricsHandler())
|
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/health", s.handleHealth)
|
||||||
s.mux.HandleFunc("GET /v1/ready", s.handleReady)
|
s.mux.HandleFunc("GET /v1/ready", s.handleReady)
|
||||||
s.mux.HandleFunc("GET /v1/version", s.handleVersion)
|
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) {
|
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
sha := strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA"))
|
ver, sha, buildTime := version.Info()
|
||||||
if sha == "" {
|
resp := map[string]string{
|
||||||
sha = "unknown"
|
"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 {
|
func moduleJSON(mod *store.Module) map[string]any {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"evobgp/internal/jobs"
|
"evobgp/internal/jobs"
|
||||||
"evobgp/internal/signing"
|
"evobgp/internal/signing"
|
||||||
|
"evobgp/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testBundleSeed = "0101010101010101010101010101010101010101010101010101010101010101"
|
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"])
|
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"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Generated
+7691
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
||||||
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
||||||
import AppNavLinks from './app-nav-links.svelte';
|
import AppNavLinks from './app-nav-links.svelte';
|
||||||
|
import AppVersion from './app-version.svelte';
|
||||||
import ThemeMenu from './theme-menu.svelte';
|
import ThemeMenu from './theme-menu.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -54,4 +55,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<AppNavLinks {collapsed} />
|
<AppNavLinks {collapsed} />
|
||||||
|
<AppVersion />
|
||||||
</aside>
|
</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}
|
||||||
@@ -100,8 +100,13 @@
|
|||||||
|
|
||||||
const versionText = $derived.by(() => {
|
const versionText = $derived.by(() => {
|
||||||
if (!version) return '—';
|
if (!version) return '—';
|
||||||
|
const ver =
|
||||||
|
(typeof version.version === 'string' && version.version) ||
|
||||||
|
(typeof version.api_version === 'string' && version.api_version) ||
|
||||||
|
'';
|
||||||
const sha =
|
const sha =
|
||||||
typeof version.git_sha === 'string' && version.git_sha ? version.git_sha : 'unknown';
|
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);
|
return sha.slice(0, 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user