# Quality gates (reusable). Callers: ci.yaml (PR), cd.yaml (push main). name: quality on: workflow_call: inputs: is_pull_request: type: boolean required: true base_sha: type: string required: false default: "" head_sha: type: string required: false default: "" before_sha: type: string required: false default: "" allow_registry_login: type: boolean required: false default: false secrets: ACTIONS_PAT: required: false docker_hub_token: required: false docker_hub_username: required: false permissions: contents: read jobs: 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: - if: ${{ inputs.is_pull_request }} uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - if: ${{ inputs.is_pull_request == false }} uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - id: detect name: Detect changed paths per module env: IS_PR: ${{ inputs.is_pull_request }} BASE_SHA: ${{ inputs.base_sha }} HEAD_SHA: ${{ inputs.head_sha }} BEFORE_SHA: ${{ inputs.before_sha }} run: | set -euo pipefail openapi=false go=false web=false bird_conf=false docker_go=false docker_web=false docker_bird=false 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 [ "$IS_PR" = "true" ]; then FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" else after="${HEAD_SHA:-$(git rev-parse HEAD)}" before="$BEFORE_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 .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 ;; .cursor/*|.claude/*|.codegraph/*|memory-bank/*) ;; *.md|AGENTS.md) ;; 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/*|.dockerignore) 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/*) ;; package.json|package-lock.json|pnpm-lock.yaml|pnpm-workspace.yaml|.releaserc.json) full_pipeline=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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: pnpm-hash run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT" - id: pnpm-cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.PNPM_STORE_DIR }} ${{ env.COREPACK_HOME }} node_modules apps/web/node_modules packages/ui/node_modules key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }} restore-keys: | pnpm-${{ runner.os }}- - name: pnpm install, Redocly, codegen check env: PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }} run: | set -euxo pipefail sh scripts/ci/pnpm-ci.sh pnpm exec redocly lint docs/openapi.yaml 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: pnpm-hash run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT" - id: pnpm-cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.PNPM_STORE_DIR }} ${{ env.COREPACK_HOME }} node_modules apps/web/node_modules packages/ui/node_modules key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }} restore-keys: | pnpm-${{ runner.os }}- - name: pnpm install, typecheck, lint, test, build env: PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }} run: | set -euxo pipefail sh scripts/ci/pnpm-ci.sh 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: "1.24" cache: false - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: go-hash run: echo "key=$(sha256sum go.sum | awk '{print $1}')" >> "$GITHUB_OUTPUT" - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.GOMODCACHE }} ${{ env.GOCACHE }} ${{ env.GOBIN }} ${{ env.GOLANGCI_LINT_CACHE }} key: go-${{ runner.os }}-1.24-gl1.64.8-${{ steps.go-hash.outputs.key }} restore-keys: | go-${{ runner.os }}-1.24-gl1.64.8- go-${{ runner.os }}-1.24- - name: Download modules env: GOMODCACHE: ${{ env.GOMODCACHE }} GOCACHE: ${{ env.GOCACHE }} run: go mod download - name: Vet env: GOFLAGS: -mod=readonly 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 - name: golangci-lint env: GOLANGCI_LINT_VERSION: v1.64.8 run: sh scripts/ci/golangci-lint.sh - name: Test env: GOFLAGS: -mod=readonly GOMODCACHE: ${{ env.GOMODCACHE }} GOCACHE: ${{ env.GOCACHE }} run: go test ./... -race -count=1 - name: Build all commands env: GOFLAGS: -mod=readonly GOMODCACHE: ${{ env.GOMODCACHE }} GOCACHE: ${{ env.GOCACHE }} 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - 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: inputs.is_pull_request runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: pnpm-hash run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT" - id: pnpm-cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.PNPM_STORE_DIR }} ${{ env.COREPACK_HOME }} node_modules apps/web/node_modules packages/ui/node_modules key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }} restore-keys: | pnpm-${{ runner.os }}- - name: Lint commit messages env: BASE_SHA: ${{ inputs.base_sha }} HEAD_SHA: ${{ inputs.head_sha }} PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }} run: | set -euxo pipefail sh scripts/ci/pnpm-ci.sh pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA" docker-check: needs: [changes] if: >- inputs.is_pull_request && (needs.changes.outputs.docker_go == 'true' || needs.changes.outputs.docker_web == 'true' || needs.changes.outputs.docker_bird == 'true') runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 with: name: evobgp driver: docker-container cleanup: false - name: Log in to Docker Hub if: secrets.docker_hub_token != '' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.docker_hub_username || gitea.actor }} password: ${{ secrets.docker_hub_token }} - name: Log in to Gitea Registry if: inputs.allow_registry_login uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: git.shx.one username: ${{ gitea.actor }} password: ${{ secrets.ACTIONS_PAT }} - name: bake --print working-directory: deploy/docker env: BUILDX_BAKE_ENTITLEMENTS_FS: "0" BUILDX_BAKE_FILE_RELATIVE_PATHS: "1" run: docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl --print default - name: bake (no push) if: inputs.allow_registry_login working-directory: deploy/docker env: BUILDX_BAKE_ENTITLEMENTS_FS: "0" BUILDX_BAKE_FILE_RELATIVE_PATHS: "1" CACHE_REF_GO: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:go-buildcache CACHE_REF_WEB: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:web-buildcache CACHE_REF_BIRDC: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:birdc-buildcache run: | set -euxo pipefail owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" export CACHE_REF_GO="git.shx.one/${owner_lc}/evobgp-buildcache:go-buildcache" export CACHE_REF_WEB="git.shx.one/${owner_lc}/evobgp-buildcache:web-buildcache" export CACHE_REF_BIRDC="git.shx.one/${owner_lc}/evobgp-buildcache:birdc-buildcache" docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl default