From 54eaad3f78f22156736277c8b66d384d7817036d Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 5 Apr 2026 22:17:29 +0700 Subject: [PATCH] refactor: enhance CI workflow for granular change detection by module and improve idempotency key generation in API client. Update job flags and conditions for better handling of file changes across different components. --- .gitea/workflows/ci.yaml | 277 +++++++++++++++++++++++++++----------- web/src/lib/api/client.ts | 20 ++- 2 files changed, 218 insertions(+), 79 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index b86c415..a9d3870 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -7,22 +7,38 @@ on: branches: [main, master] jobs: + # --------------------------------------------------------------------------- + # Гранулярная детекция изменений по модулям. + # Каждый флаг соответствует группе файлов; downstream-джобы запускаются + # только когда их группа затронута. Изменение CI-конфигурации (.gitea/workflows/*) + # поднимает все флаги, чтобы гарантировать полный прогон. + # --------------------------------------------------------------------------- changes: runs-on: ubuntu-latest outputs: - openapi: ${{ steps.detect.outputs.openapi }} - code: ${{ steps.detect.outputs.code }} + 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 (OpenAPI vs doc-only vs code) + name: Detect changed paths per module run: | set -euo pipefail openapi=false - code=true + go=false + web=false + bird_conf=false + docker_go=false + docker_web=false + docker_bird=false if [ "${{ github.event_name }}" = "pull_request" ]; then base="${{ github.event.pull_request.base.sha }}" @@ -36,52 +52,61 @@ jobs: elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then FILES="$(git diff --name-only HEAD~1 HEAD)" else - # Нет родителя (первый коммит / тонкий clone) — полный пайплайн + OpenAPI - openapi=true - code=true - echo "openapi=$openapi" >> "$GITHUB_OUTPUT" - echo "code=$code" >> "$GITHUB_OUTPUT" - echo "No parent commit: openapi=true code=true" + openapi=true; go=true; web=true; bird_conf=true + docker_go=true; docker_web=true; docker_bird=true + for v in openapi go web bird_conf docker_go docker_web docker_bird; do + echo "$v=true" >> "$GITHUB_OUTPUT" + done + echo "No parent commit — full pipeline" exit 0 fi fi - # Пустой diff (редко) — не отключаем сборку из осторожности if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then - openapi=false - code=true - echo "openapi=$openapi" >> "$GITHUB_OUTPUT" - echo "code=$code" >> "$GITHUB_OUTPUT" - echo "Empty diff: openapi=false code=true" + go=true; web=true + for v in openapi go web bird_conf docker_go docker_web docker_bird; do + eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT" + done + echo "Empty diff — safe fallback: go=true web=true" exit 0 fi - if printf '%s\n' "$FILES" | grep -qE '^docs/openapi\.yaml$|^redocly\.yaml$'; then - openapi=true - fi + ci_changed=false - doc_only_all=true while IFS= read -r f || [ -n "${f:-}" ]; do [ -z "${f:-}" ] && continue - if [[ "$f" == "README.md" || "$f" == ".gitea/README.md" || "$f" == "web/README.md" || "$f" == "redocly.yaml" || "$f" == docs/* ]]; then - continue - fi - doc_only_all=false - break + case "$f" in + .gitea/workflows/*) ci_changed=true ;; + docs/openapi.yaml|redocly.yaml) openapi=true ;; + web/README.md) ;; # doc-only + web/*) web=true ;; + deploy/bird/*) bird_conf=true ;; + deploy/docker/gobinary/*) docker_go=true ;; + deploy/docker/evobgp-agent/*) docker_go=true ;; + deploy/docker/evobgp-web/*) docker_web=true ;; + deploy/docker/bird2/*) docker_bird=true ;; + go.mod|go.sum|go.work) go=true ;; + *.go) go=true ;; + cmd/*|internal/*) go=true ;; + esac done <<< "$FILES" - if $doc_only_all; then - code=false - else - code=true + if $ci_changed; then + go=true; web=true; bird_conf=true + docker_go=true; docker_web=true; docker_bird=true fi - echo "openapi=$openapi" >> "$GITHUB_OUTPUT" - echo "code=$code" >> "$GITHUB_OUTPUT" - echo "Changed files (first 20):" - printf '%s\n' "$FILES" | head -n 20 - echo "openapi=$openapi code=$code" + for v in openapi go web bird_conf docker_go docker_web docker_bird; do + eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT" + done + 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 ci=$ci_changed" + + # --------------------------------------------------------------------------- openapi: needs: [changes] if: needs.changes.outputs.openapi == 'true' @@ -94,9 +119,10 @@ jobs: - name: Lint OpenAPI (Redocly) run: npx --yes @redocly/cli@1 lint docs/openapi.yaml + # --------------------------------------------------------------------------- go: needs: [changes] - if: needs.changes.outputs.code == 'true' + if: needs.changes.outputs.go == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -117,13 +143,12 @@ jobs: go build -o "$out/$name" "./$d" done + # --------------------------------------------------------------------------- bird2: runs-on: ubuntu-latest needs: [go] steps: - uses: actions/checkout@v4 - # Вложенный docker + bind-mount ломается, если демон Docker на хосте не видит путь - # workspace runner'а (/workspace/...). bird -p на самом job-раннере надёжнее. - name: Install bird2 run: | set -euxo pipefail @@ -148,10 +173,20 @@ jobs: bird -c "$WS/$conf" -p done - # Сборка образов и push в Container Registry Gitea (только push в main/master; см. .gitea/README.md). - docker-images: - needs: [go] - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + # --------------------------------------------------------------------------- + # Docker: Go-бинарники (api, all, scheduler, ingest, render, deploy, node, agent). + # Запускается если изменился Go-код или Go-Dockerfile. + # Если Go-код менялся — требуем успех go-тестов; если только Dockerfile — go skipped, ОК. + # --------------------------------------------------------------------------- + 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 strategy: fail-fast: false @@ -192,24 +227,13 @@ jobs: bin: evobgp-node birdc: "0" evobgp_upstream: "" - - image: evobgp-web - dockerfile: deploy/docker/evobgp-web/Dockerfile - evobgp_upstream: "" - - image: evobgp-web-all - dockerfile: deploy/docker/evobgp-web/Dockerfile - evobgp_upstream: evobgp-all - image: evobgp-agent dockerfile: deploy/docker/evobgp-agent/Dockerfile evobgp_upstream: "" - - image: evobgp-bird2 - dockerfile: deploy/docker/bird2/Dockerfile - evobgp_upstream: "" steps: - uses: actions/checkout@v4 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Prepare image metadata id: meta run: | @@ -218,22 +242,16 @@ jobs: echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT" short_sha="$(echo '${{ github.sha }}' | cut -c1-7)" echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT" - echo "docker_push=true" >> "$GITHUB_OUTPUT" - - name: Log in to Gitea Registry - if: steps.meta.outputs.docker_push == 'true' uses: docker/login-action@v3 with: registry: git.shts.su username: ${{ gitea.actor }} - # PAT репозитория или встроенный токен job (нужны права на пакеты в настройках Actions). password: ${{ secrets.ACTIONS_PAT || gitea.token }} - - name: Build and push ${{ matrix.image }} env: OWNER_LC: ${{ steps.meta.outputs.owner_lc }} SHORT_SHA: ${{ steps.meta.outputs.short_sha }} - DO_PUSH: ${{ steps.meta.outputs.docker_push }} DF: ${{ matrix.dockerfile }} IMAGE: ${{ matrix.image }} BIN: ${{ matrix.bin }} @@ -244,7 +262,7 @@ jobs: WS="${{ github.workspace }}" cd "$WS" - TAG_LOCAL="evobgp:${IMAGE}-${SHORT_SHA}" + IMG="git.shts.su/${OWNER_LC}/${IMAGE}" BUILD_ARGS=() if [ -n "${BIN:-}" ]; then BUILD_ARGS+=(--build-arg "BIN=${BIN}") @@ -254,23 +272,126 @@ jobs: BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}") fi - if [ "$DO_PUSH" = "true" ]; then - IMG="git.shts.su/${OWNER_LC}/${IMAGE}" - docker buildx build \ - --platform linux/amd64 \ - --file "$DF" \ - "${BUILD_ARGS[@]}" \ - --tag "${IMG}:latest" \ - --tag "${IMG}:${SHORT_SHA}" \ - --tag "${IMG}:sha-${{ github.sha }}" \ - --push \ - "$WS" - else - docker buildx build \ - --platform linux/amd64 \ - --file "$DF" \ - "${BUILD_ARGS[@]}" \ - --tag "$TAG_LOCAL" \ - --load \ - "$WS" + docker buildx build \ + --platform linux/amd64 \ + --file "$DF" \ + "${BUILD_ARGS[@]}" \ + --tag "${IMG}:latest" \ + --tag "${IMG}:${SHORT_SHA}" \ + --tag "${IMG}:sha-${{ github.sha }}" \ + --push \ + "$WS" + + # --------------------------------------------------------------------------- + # Docker: Web-фронтенд (evobgp-web, evobgp-web-all). + # Не зависит от Go-тестов — SvelteKit собирается отдельно. + # --------------------------------------------------------------------------- + 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 + strategy: + fail-fast: false + matrix: + include: + - image: evobgp-web + dockerfile: deploy/docker/evobgp-web/Dockerfile + evobgp_upstream: "" + - image: evobgp-web-all + dockerfile: deploy/docker/evobgp-web/Dockerfile + evobgp_upstream: evobgp-all + 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 ${{ matrix.image }} + env: + OWNER_LC: ${{ steps.meta.outputs.owner_lc }} + SHORT_SHA: ${{ steps.meta.outputs.short_sha }} + DF: ${{ matrix.dockerfile }} + IMAGE: ${{ matrix.image }} + EVOBGP_UPSTREAM: ${{ matrix.evobgp_upstream }} + run: | + set -euxo pipefail + WS="${{ github.workspace }}" + cd "$WS" + + IMG="git.shts.su/${OWNER_LC}/${IMAGE}" + BUILD_ARGS=() + if [ -n "${EVOBGP_UPSTREAM:-}" ]; then + BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}") fi + + docker buildx build \ + --platform linux/amd64 \ + --file "$DF" \ + "${BUILD_ARGS[@]}" \ + --tag "${IMG}:latest" \ + --tag "${IMG}:${SHORT_SHA}" \ + --tag "${IMG}:sha-${{ github.sha }}" \ + --push \ + "$WS" + + # --------------------------------------------------------------------------- + # 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 + env: + OWNER_LC: ${{ steps.meta.outputs.owner_lc }} + SHORT_SHA: ${{ steps.meta.outputs.short_sha }} + run: | + set -euxo pipefail + WS="${{ github.workspace }}" + IMG="git.shts.su/${OWNER_LC}/evobgp-bird2" + + docker buildx build \ + --platform linux/amd64 \ + --file deploy/docker/bird2/Dockerfile \ + --tag "${IMG}:latest" \ + --tag "${IMG}:${SHORT_SHA}" \ + --tag "${IMG}:sha-${{ github.sha }}" \ + --push \ + "$WS" diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index 961192d..06758af 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -27,6 +27,24 @@ function mergeHeaders(init?: RequestInit, extraHeaders?: Record) return h; } +/** + * Idempotency keys: `crypto.randomUUID()` exists only in secure contexts (HTTPS / localhost). + * Over plain HTTP to a LAN IP it is often undefined — use getRandomValues or a fallback. + */ +function newIdempotencyKey(): string { + const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined; + if (c?.randomUUID) return c.randomUUID(); + if (c?.getRandomValues) { + const buf = new Uint8Array(16); + c.getRandomValues(buf); + buf[6] = (buf[6]! & 0x0f) | 0x40; + buf[8] = (buf[8]! & 0x3f) | 0x80; + const hex = [...buf].map((b) => b.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; + } + return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`; +} + export class ApiError extends Error { constructor( public readonly status: number, @@ -58,7 +76,7 @@ export async function apiMutate( const headers: Record = {}; if (body !== undefined) headers['Content-Type'] = 'application/json'; if (opts?.idempotent !== false) { - headers['Idempotency-Key'] = crypto.randomUUID(); + headers['Idempotency-Key'] = newIdempotencyKey(); } const res = await fetch(path, { method,