name: CI on: push: branches: [main, master] pull_request: branches: [main, master] jobs: # --------------------------------------------------------------------------- # Детекция изменений по модулям (флаги → downstream-джобы в графе CI). # Полный прогон (все флаги true): .gitea/workflows/*, scripts/*, .golangci.yml, # .pre-commit-config.yaml — чтобы при правках CI/CD пересобирались все узлы. # --------------------------------------------------------------------------- changes: runs-on: ubuntu-latest outputs: openapi: ${{ steps.detect.outputs.openapi }} go: ${{ steps.detect.outputs.go }} web: ${{ steps.detect.outputs.web }} bird_conf: ${{ steps.detect.outputs.bird_conf }} docker_go: ${{ steps.detect.outputs.docker_go }} docker_web: ${{ steps.detect.outputs.docker_web }} docker_bird: ${{ steps.detect.outputs.docker_bird }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - id: detect name: Detect changed paths per module run: | set -euo pipefail openapi=false go=false web=false bird_conf=false docker_go=false docker_web=false docker_bird=false # Все флаги true → openapi, web, go, bird2 (и release на main) в графе CI. set_all_flags_true() { openapi=true go=true web=true bird_conf=true docker_go=true docker_web=true docker_bird=true } write_outputs() { for v in openapi go web bird_conf docker_go docker_web docker_bird; do eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT" done } if [ "${{ github.event_name }}" = "pull_request" ]; then base="${{ github.event.pull_request.base.sha }}" head="${{ github.event.pull_request.head.sha }}" FILES="$(git diff --name-only "$base" "$head")" else before="${{ github.event.before }}" after="${{ github.sha }}" if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ]; then FILES="$(git diff --name-only "$before" "$after")" elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then FILES="$(git diff --name-only HEAD~1 HEAD)" else set_all_flags_true write_outputs echo "No parent commit — full pipeline (all modules)" exit 0 fi fi if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then set_all_flags_true write_outputs echo "Empty diff — full pipeline fallback" exit 0 fi full_pipeline=false while IFS= read -r f || [ -n "${f:-}" ]; do [ -z "${f:-}" ] && continue case "$f" in # CI/CD инфраструктура — все узлы quality gates .gitea/workflows/*|.golangci.yml|.pre-commit-config.yaml|scripts/*) full_pipeline=true ;; docs/openapi.yaml|redocly.yaml) openapi=true ;; docs/api.md|docs/access.md) openapi=true go=true ;; apps/web/README.md|apps/web/components.json|packages/ui/components.json) ;; apps/web/*|packages/ui/*|packages/shared/*) web=true ;; deploy/bird/*) bird_conf=true go=true ;; deploy/compose/*|deploy/docker/*) docker_go=true docker_web=true docker_bird=true go=true ;; go.mod|go.sum|go.work) go=true ;; migrations/*) go=true ;; cmd/*|internal/*|*.go) go=true bird_conf=true ;; docs/*) go=true ;; package.json|package-lock.json|pnpm-lock.yaml|pnpm-workspace.yaml|.releaserc.json) full_pipeline=true ;; *) go=true ;; esac done <<< "$FILES" if $full_pipeline; then set_all_flags_true fi write_outputs echo "Changed files (first 30):" printf '%s\n' "$FILES" | head -n 30 echo "--- flags ---" echo "openapi=$openapi go=$go web=$web bird_conf=$bird_conf" echo "docker_go=$docker_go docker_web=$docker_web docker_bird=$docker_bird full_pipeline=$full_pipeline" # --------------------------------------------------------------------------- openapi: needs: [changes] if: needs.changes.outputs.openapi == 'true' || needs.changes.outputs.web == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" - name: Enable pnpm via corepack run: corepack enable - name: Lint OpenAPI (Redocly) run: npx --yes @redocly/cli@1 lint docs/openapi.yaml - name: Check OpenAPI→TS codegen is fresh run: | set -euxo pipefail pnpm install --frozen-lockfile chmod +x scripts/check-openapi-gen.sh sh scripts/check-openapi-gen.sh # --------------------------------------------------------------------------- web: needs: [changes] if: needs.changes.outputs.web == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" - name: Enable pnpm via corepack run: corepack enable - name: pnpm install, typecheck, lint, test, build run: | set -euxo pipefail pnpm install --frozen-lockfile pnpm --filter @evobgp/web run typecheck pnpm --filter @evobgp/web run lint pnpm --filter @evobgp/web run test pnpm --filter @evobgp/web run build # --------------------------------------------------------------------------- go: needs: [changes] if: needs.changes.outputs.go == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.24" cache: true cache-dependency-path: go.sum - name: Vet run: go vet ./... - name: Lint httpapi (ERR-01 / ARCH-01) run: sh scripts/lint-httpapi.sh - name: Check migration pairs (DEP-03) run: sh scripts/check-migrations-pair.sh - name: Validate remote speaker compose run: sh scripts/validate-remote-speaker-compose.sh # go.mod: go 1.24 — бинарник golangci-lint < v1.64.2 (сборка на Go 1.23) не запускается. - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.64.8 install-mode: goinstall - name: Test run: go test ./... -race -count=1 - name: Build all commands run: | set -euxo pipefail out="${RUNNER_TEMP}/evobgp-bin" mkdir -p "$out" for d in cmd/*/; do name="$(basename "$d")" go build -o "$out/$name" "./$d" done # --------------------------------------------------------------------------- bird2: runs-on: ubuntu-latest needs: [changes, go] if: >- always() && needs.changes.result == 'success' && needs.go.result != 'failure' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.bird_conf == 'true' || needs.changes.outputs.docker_bird == 'true' || needs.changes.outputs.docker_go == 'true') steps: - uses: actions/checkout@v4 - name: Install bird2 (репозиторий Ubuntu runner, как в образе evobgp-bird2) run: | set -euxo pipefail if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=""; fi $SUDO apt-get update -qq DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y -qq bird2 bird --version - name: bird -p on all scenario bird.conf files env: WORKSPACE: ${{ github.workspace }} run: | set -euxo pipefail WS="${WORKSPACE:-$PWD}" cd "$WS" if [ ! -f internal/birdfmt/testdata/scenarios/minimal/bird.conf ]; then echo "Нет сценариев BIRD в checkout. Проверьте, что internal/birdfmt/testdata/scenarios закоммичен и push в remote." ls -la internal/birdfmt/testdata/ 2>/dev/null || ls -la exit 1 fi for conf in internal/birdfmt/testdata/scenarios/*/bird.conf; do echo "==> $conf" bird -c "$WS/$conf" -p done # --------------------------------------------------------------------------- commitlint: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: 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 npm ci npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}" # --------------------------------------------------------------------------- # Один push в main: semantic-release (тег на текущий commit, без доп. commit) + docker push. # --------------------------------------------------------------------------- release: needs: [changes, openapi, web, go, bird2] if: >- always() && github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && needs.changes.result == 'success' && (needs.openapi.result == 'success' || needs.openapi.result == 'skipped') && (needs.web.result == 'success' || needs.web.result == 'skipped') && (needs.go.result == 'success' || needs.go.result == 'skipped') && (needs.bird2.result == 'success' || needs.bird2.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true 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: Verify releasable commit messages run: node scripts/commit/verify-release-commits.mjs - name: Semantic release run: npx semantic-release env: GITEA_URL: https://git.shx.one GITEA_TOKEN: ${{ secrets.ACTIONS_PAT || gitea.token }} - name: Detect new release id: rel run: | set -euo pipefail version="" if [ -f .release-version ]; then version="$(tr -d '[:space:]' < .release-version)" echo "New release from semantic-release: $version" else # Re-run after a failed docker step: tag already exists, successCmd # did not write .release-version (semantic-release is a no-op). git fetch --tags --force origin || true tag="$(git tag --points-at HEAD --list 'v*.*.*' | sort -V | tail -n1 || true)" if [ -n "${tag:-}" ]; then version="${tag#v}" echo "Reuse existing tag $tag on HEAD (release retry)" fi fi if [ -n "${version:-}" ]; then echo "version=$version" >> "$GITHUB_OUTPUT" echo "released=true" >> "$GITHUB_OUTPUT" else echo "released=false" >> "$GITHUB_OUTPUT" echo "No releasable commits — skipping image publish" fi - name: Set up Docker Buildx if: steps.rel.outputs.released == 'true' uses: docker/setup-buildx-action@v3 - name: Prepare image metadata if: steps.rel.outputs.released == 'true' id: meta run: | set -euo pipefail echo "version=${{ steps.rel.outputs.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 if: steps.rel.outputs.released == 'true' uses: docker/login-action@v3 with: registry: git.shx.one username: ${{ gitea.actor }} password: ${{ secrets.ACTIONS_PAT || gitea.token }} - name: Build and push images (bake) if: steps.rel.outputs.released == 'true' env: REGISTRY: git.shx.one/${{ 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.shx.one/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:go-buildcache CACHE_REF_WEB: git.shx.one/${{ 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